DIY TCP/IP TCP模块的实现5

上一篇:DIY TCP/IP TCP模块的实现4
9.7 TCP三步握手的实现2
本节基于9.6节对TCP三步握手的分析,扩展9.5节的代码,实现TCP的三步握手。具体来说是扩展9.5节的process_tcp_syn函数。DIY TCP/IP的TCP模块是TCP Server,监听指定端口,在TCP模块正确接收TCP SYN时,解析TCP SYN并保存对应的信息。构建TCP SYN-ACK头部,添加相应的选项字段,构造伪头部,计算头部校验和,完成TCP SYN-ACK数据帧的构造后,通过IP模块的接口发送TCP SYN-ACK数据帧。如果TCP模块能接收到TCP Client回复的TCP ACK,并触发连接状态机转换到STATE_ESTABLISHED状态,即说明构造的TCP SYN-ACK是正确的。再判断接收到的TCP ACK数据帧头部的SYN数值是否是发出的TCP SYN-ACK的序号syn 加上 1,如果是,则说明收到的TCP ACK是对TCP SYN-ACK的确认,即验证TCP三步握手建立连接的代码实现是正确的。
先回顾一下9.5节的代码实现,IP模块接收到TCP数据帧后,调用TCP模块的tcppkt_recv,该函数根据TCP头部构造伪头部,检验TCP头部校验和。检验通过后,根据TCP SYN头部的目标端口号查找TCP连接表,找到与目标端口对应的尚未完成三步握手建立连接的tcp_conn_t结构的指针。将收到的TCP数据帧交给tcp_processs_conn_pkt函数处理。tcp_process_conn_pkt根据当前连接所处的状态STATE_LISTEN,调用process_tcp_syn处理接收到的TCP SYN数据帧。9.5节仅仅是简单的调用tcp_parse_options解析并保存了TCP SYN数据帧头部选项字段的值,下面就来扩展该函数,完成对TCP SYN数据帧的处理。

 static int process_tcp_syn(tcp_conn_t *conn,
         tcphdr_t *syn, unsigned short syn_len)
 {
 /* MSS + 3*NOP + TIMESTAMP + WINDOW_SCALE */
 #define OPT_SZ (4 + 3 + 10 + 3)
     int ret = 0;
     pdbuf_t *pdbuf = NULL;
     tcphdr_t *synack = NULL;
     unsigned short synack_len = 0;
     tcphdr_flags_t synack_flags;
     optmap_t opts;
 
     if (conn == NULL || syn == NULL || syn_len == 0)
             return -1;
     if (tcp_parse_options(conn, syn, syn_len)) {
         log_printf(ERROR, "Failed to parse TCP options\n");
         ret = -1;
         goto out;
     }
     conn->send_seq = 0;
     conn->ack_seq = get_acknum(syn, syn_len) + 1;
 
     synack_len = sizeof(tcphdr_t) + OPT_SZ;
     pdbuf = pdbuf_alloc(synack_len, 0);
     if (pdbuf == NULL) {
         log_printf(ERROR, "No memory for tcp packet\n");
         ret = -1;
         goto out;
     }
     pdbuf_push(pdbuf, synack_len);
     synack = (tcphdr_t *)pdbuf->payload;
     synack_flags.v = 0;
     synack_flags.b.syn = 1;
     synack_flags.b.ack = 1;
     synack_flags.b.hdr_len = (synack_len >> 2);
     if (ret = build_tcp_header(conn,
             synack, 0, synack_flags.v)) {
         log_printf(ERROR, "Failed to build tcp header\n");
         goto out;
     }
     opts.v = 0;
     opts.b.mss = 1;
     if (conn->time_echo)
         opts.b.ts = 1;
     if (conn->l_shift_cn)
         opts.b.ws = 1;
     if ((ret = tcp_add_options(opts.v,
             conn, synack, OPT_SZ)) < 0) {
         log_printf(ERROR, "Failed to add tcp options\n");
         goto out;
     }
     return tcppkt_send(conn, pdbuf);
 out:
     if (pdbuf)
         pdbuf_free(pdbuf);
     return ret;
 }

process_tcp_syn是TCP模块的静态函数,只在TCP模块内部使用,处理接收到的TCP SYN数据帧。函数的第一个参数是tcp_conn_t指针,指向与TCP SYN目标端口对应的TCP连接,第二个参数指向接收到的TCP SYN数据帧的首字节地址,第三个参数是TCP SYN数据帧的长度。这三个参数均由tcp_process_conn_pkt传入,成功返回值为0,失败返回非0。
Line 5: 宏定义OPT_SZ的值为(4 + 3 + 10 + 3),定义构造TCP SYN-ACK数据帧的选项字段的长度。第一个4是MSS选项的长度,第二个3是3个NOP选项的长度,第三个10是TIMESTAMP选项的长度,第四个3是window scale选项的长度。由于TCP头部的header length是以4个字节为单位的,即TCP头部的长度是4的整数倍。不带任何选项字段的TCP头部长度为20个字节,所以添加的选项字段总长度也应是4的整数倍。MSS,TIMESTAMP和window scale选项的长度为17,再添加3个长度为1的NOP选项,使选项字段的总长度为20。
Line 6-12: 定义函数内部使用的变量,ret为返回值,pdbuf指向pdbuf模块申请的内存空间,存放SYN-ACK数据帧。synack为SYN-ACK的TCP头部的指针,synack_len是SYN-ACK的TCP头部长度。opts是新增加的数据类型optmap_t,用于指定需要添加的选项字段。TCP模块处理不同的TCP数据帧时,需要根据不同的条件添加相应的选项字段,为统一代码实现添加选项字段的函数,将需要添加的选项抽象为位图,optmap_t类型,在处理TCP数据帧时,首相将需要添加的选项字段在选项位图中置位,再统一交给添加选项字段的函数处理。
Line 15-19: 判断入参conn,syn不为空且syn_len不为0时,继续执行。调用tcp_parse_options解析TCP SYN数据帧的头部,并保存选项字段的值。tcp_parse_options函数与9.4节的实现一致,将TCP SYN数据帧中头部的源端口,目标端口,flags字段,window size,以及头部选项字段的window scale,MSS,SACK,timestamp的数值保存在conn结构对应的成员中。
Line 20-22: 设置TCP连接conn中的开始序号为0,做为即将构造的TCP SYN-ACK数据帧的syn序号。conn->ack_seq为希望收到的下一个TCP数据帧的序号,get_acknum是新增函数,返回值是收到的TCP数据帧头部的序号syn,和TCP数据帧携带的数据部分的长度的加和。由于接收到的TCP SYN携带的数据部分的长度为0,TCP SYN本身消耗掉了一个SYN序号,所以在get_acknum的返回值后加1,做为即将构造的SYN-ACK头部的ack字段的值,确认收到TCP SYN,并且告知对方希望希望收到的下一个TCP数据帧的序号为TCP SYN的序号加1。
Line 23-29: 初始化SYN-ACK的长度为20个字节的TCP头部,加上20个字节的选项字段,共40个字节,全部是头部数据,头部后面不携带任何数据。向pdbuf模块申请内存,不忽略MTU的限制,pdbuf_alloc返回值不为空时继续执行。
Line 30-35: pdbuf_push调整pdbuf->payload向地址减少的方向移动40个字节,用于存放SYN-ACK的数据。初始化TCP头部的flags字段,设置flags字段的bit1和bit4为1,表明构造的TCP数据帧是SYN-ACK,设置TCP头部的长度为synack_len除以4。
Line 36-40: 调用新增函数build_tcp_header构造TCP头部,参数是对应的TCP连接结构体conn的指针,synack指向存放SYN-ACK内存的首字节地址,SYN-ACK携带的数据部分的长度0,以及SYN-ACK的TCP头部flags字段。
Line 41-51: 利用选项位图,设置需要向SYN-ACK的头部添加的选项字段。首先是设置MSS选项,用于通知对方本地TCP连接的MSS值。如果接收的TCP SYN的选项字段带有时间戳,tcp_parse_options时已经将时间戳字段赋值给了conn->time_echo,则添加时间戳选项。如果TCP连接对应的本地window scale值不为0,回顾9.4节,打开TCP连接监听指定端口时,已经将TCP连接对应的本地window scale设置为8,则添加window scale选项。将设置好的选项位图传给tcp_add_options添加MSS,TIMESTAMP和Window Scale选项字段。tcp_add_options是新增函数,参数为选项位图的值,对应的TCP连接,TCP数据帧的指针synack,以及TCP数据帧中的选项字段空间的大小OPT_SZ。
Line 52-57: 调用tcppkt_send发送构造好的SYN-ACK数据帧,tcppkt_send是TCP模块的新增函数,用于将TCP数据帧发送到IP模块。执行到line529时,说明已经出现错误了,释放申请的pdbuf,返回非0值。
再来逐一介绍process_tcp_syn中用到的新增函数get_acknum,build_tcp_header,tcp_add_options以及tcppkt_send。这些函数实现均在TCP模块的tcp.c文件中。

 static inline unsigned int get_acknum(tcphdr_t *tcppkt,
                 unsigned short tcppkt_len)
 {
     tcphdr_flags_t flags;
     unsigned short data_len;
 
     if (tcppkt == NULL || tcppkt_len == 0)
         return 0;
     flags.v = NTOHS(tcppkt->flags_len);
     data_len = tcppkt_len - (flags.b.hdr_len << 2);
     return NTOHL(tcppkt->seq_num) + data_len;
 }

get_acknum在接收到远端TCP发送的TCP数据帧时,根据TCP头部的序号,和TCP数据帧携带的数据部分的长度,计算本地TCP端回复ACK数据帧的ack数值。根据9.6节的分析,本地TCP端收到TCP数据帧时,应将收到的TCP数据帧头部的序号(记为syn_x)与TCP数据帧携带的数据部分的长度(记为len_x)加和,即syn_x + len_x做为回复ACK数据帧的ack数值,表明接收到了从syn_x开始到syn_x + len_x - 1的字节,希望收到的下一个字节的序号为syn_x + len_x。
get_acknum的第一个参数为TCP模块接收到的TCP数据帧的首字节地址,第二个参数为TCP数据帧的长度,包括头部和携带的数据部分的长度。TCP头部的长度字段乘以4,得到TCP头部的长度,再用TCP数据帧的总长度减去头部长度,得到数据部分的长度。最后返回TCP头部的序号与TCP数据部分的长度的和。
get_acknum在接收到每个TCP数据帧时均被调用,实现比较简短,定义为内联函数可以减少TCP模块接收到TCP数据帧的函数调用次数,提高运行效率。

 static int build_tcp_header(tcp_conn_t *conn,
                 tcphdr_t *tcphdr, unsigned short data_len,
                 unsigned short hflags)
 {
         int ret = 0;
         if (conn == NULL || tcphdr == NULL)
                 return -1;
         tcphdr->src_port = HSTON(conn->l_port);
         tcphdr->dst_port = HSTON(conn->r_port);
         tcphdr->seq_num = HLTON(conn->send_seq);
         tcphdr->ack_num = HLTON(conn->ack_seq);
         tcphdr->flags_len = HSTON(hflags);
         tcphdr->wnd_sz = HSTON(conn->l_wnd_sz);
         tcphdr->cksum = 0;
         tcphdr->ugtp = 0;
         return ret;
 }

TCP模块需要为待发送的TCP数据帧构造TCP头部,将这些公用的操作封装成build_tcp_headr函数方便调用。build_tcp_header的参数为TCP连接指针conn,需要构造头部的TCP数据帧的首字节地址tcphdr,TCP数据帧的长度data_len,预先准备好的头部flags字段。
函数的实现即为TCP头部的各个字段赋值,源端口和目的端口在处理TCP三步握手的数据帧时,通过解析选项tcp_parse_options将源端口和目的端口的数值保存在连接结构体中。conn->seq_num是TCP本地端发送TCP数据帧的序号,该序号在build_tcp_header的各个上下文中设置.例如本节在process_tcp_syn中将该数值初始化为0。其他调用build_tcp_header的上下文应将conn->seq_num设置为正确的值。同样conn->ack_seq,以及头部hflags字段都是在build_tcp_header的调用上下文设置为正确的值,例如本节process_tcp_syn将ack设置为TCP SYN的序号加1,hflags的bit1和bit4置位,TCP的头部长度设置为synack_len >> 4。
conn->l_wnd_sz是TCP连接本地端的接收缓存大小,在打开TCP连接的函数tcp_conn_open中,该值被设置为65535。cksum和ugtp字段的数值分别初始化为0。build_tcp_header并没有计算头部的校验和字段,因为build_tcp_header的调用上下文可能还没有为要发送的TCP数据帧添加选项字段。而TCP头部校验和的计算应包含伪头部,以及TCP头部,头部选项字段,以及TCP头部后面携带的所有数据。所以将校验和的计算放在发送TCP数据帧的函数tcppkt_send中。

 static int tcp_add_options(int optmap, tcp_conn_t *conn,
         tcphdr_t *tcppkt, unsigned int opt_sz)
 {
     int ret = 0;
     optmap_t opts;
     int opt_len = 0;
 
     if (tcppkt == NULL || opt_sz == 0)
         return -1;
     opts.v = optmap;
     if (opts.b.mss) {
         unsigned short mss = HSTON(conn->l_mss);
         ret = add_opt(tcppkt->options, opt_sz,
             TCP_OPT_MSS, &mss, sizeof(mss));
         if (ret < 0)
             goto out;
         opt_len += ret;
     }
     if (opts.b.ts) {
         int ts[2];
         ts[0] = NTOHL(get_current_time());
         ts[1] = NTOHL(conn->time_echo);
         ret = add_opt(tcppkt->options, opt_sz,
             TCP_OPT_TIMESTAMP, ts, sizeof(ts));
         if (ret < 0)
             goto out;
         opt_len += ret;
     }
     if (opts.b.ws) {
         unsigned char ws = conn->l_shift_cn;
         ret = add_opt(tcppkt->options, opt_sz,
             TCP_OPT_WSCALE, &ws, sizeof(ws));
         if (ret < 0)
             goto out;
         opt_len += ret;
     }
     if (opt_len % 4) {
         ret = align_opt(tcppkt, opt_sz);
         if (ret < 0)
             goto out;
         opt_len += ret;
     }
     ret = opt_len;
 out:
     return ret;
 }

tcp_add_options的第一个参数为选项位图optmap,第二个参数是TCP连接指针conn,第三个参数是需要添加选项的TCP数据帧的首字节地址,第四个参数是TCP数据帧中预留给选项字段的空间大小。
选项位图结构体optmap_t的定义

  typedef union _optmap {
      struct {
          unsigned int mss:1; /* bit0 */
          unsigned int sack:1;    /* bit1 */
          unsigned int ts:1;  /* bit2 */
          unsigned int nop:1; /* bit3 */
          unsigned int ws:1;  /* bit4 */
          unsigned int rsvd:27;   /* reserved */
      } b;
      unsigned int v;
  } optmap_t;

optmap_t为union类型,成员v为选项位图的整型值,成员b是整型值v的32个bit的定义。bit0到bit4分别代表mss, sack, timestamp, nop, window scale共5个TCP选项,成员b的rsvd占27个bit,用于扩展其他选项。
Line 8-18: 将选项位图的整型数值optmap,赋值给局部变量opts.v,解析选项位图中的各个bit。如果选项位图中的bit0置位,则调用add_opt添加MSS选项,conn->l_mss是TCP连接本地端的MSS数值。在调用tcp_conn_open打开TCP连接监听指定端口时,根据eth0接口的MTU计算得到具体数值,并存放在conn->l_mss字段。
Line 19-28: 如果选项位图中的bit2置位,则调用add_opt添加TIMESTAMP选项。结合process_tcp_syn的调用上下文,当收到TCP SYN后,tcp_parse_options解析TCP SYN的头部选项,将TSval的数值存放在conn->time_cho字段。tcp_add_options在process_tcp_syn的调用上下文中通过选项位图,指定添加TIMESTAMP字段,此时才进入该分支执行,TIMESTAMP选项的Value由两个整型数值构成,第一个整型数值是本地时间戳,通过新增函数get_current_time获得。第二个整型数值是接收到的TCP数据帧头部的TSval数值,结合process_tcp_syn的上下文,即TCP SYN的时间戳选项的TSval数值。
Line 29-36: 如果选项位图中的bit4置位,则调用add_opt添加Window Scale选项。conn->l_shift_cn存放TCP连接本地端的window scale数值,该数值在调用tcp_conn_open打开TCP连接监听指定端口时,被初始化为8。
Line 37-46: 如果添加的所有选项字段的长度不是4的整数倍,则调用align_opt补充添加NOP选项。回顾9.1节TCP头部中的长度的定义,该长度是4的整数倍,不携带任何TCP选项的TCP头部为20个字节,所以添加选项后头部长度还应是4的整数倍。tcp_add_options返回添加的所有选项字段的长度。
tcp_add_options中line 440-465的三个分支都会调用add_opt构造并添加相应的选项值,以MSS为例介绍add_opt函数的实现。line441将conn->l_mss转换成大端数值,将tcppkt->options头部选项的首字节地址,tcppkt头部中选项空间的大小opt_sz,选项类型TCP_OPT_MSS,mss的大端数值,和mss数值占用的内存空间的大小传给add_opt函数。结合process_tcp_syn的调用上下文,tcppkt->options指向构造的synack的TCP头部选项字段的首字节地址。opt_sz是synack数据帧预留的20个字节的头部选项内存空间。

 static int add_opt(void *options, unsigned int opt_sz,
         unsigned char opt_type, void *value,
         unsigned int val_sz)
 {
     unsigned char *popt = NULL;
     int left_sz = 0;
 
     if (options == NULL || opt_sz == 0) {
         log_printf(ERROR,
         "invalid parameter for option: %u", opt_type);
         return -1;
     }
     popt = (unsigned char *)options;
     left_sz = opt_sz;
     /* check available space in options */
     while (left_sz > 0) {
         if (popt[0] == 0)
             break;
         else if (popt[0] == TCP_OPT_NOP) {
             popt += 1;
             left_sz -= 1;
         } else {
             popt += popt[1];
             left_sz -= popt[1];
         }
     }
     if (left_sz < (val_sz + 2)) {
         log_printf(ERROR,
         "No space for option: %u\n", opt_type);
         return -1;
     }
     /* type */
     if (opt_type == TCP_OPT_NOP) {
         popt[0] = opt_type;
         return 1;
     }
     popt[0] = opt_type;
     /* length */
     popt[1] = val_sz + 2;
     /* value */
     if (value)
         memcpy(&popt[2], value, val_sz);
     return popt[1];
 }

add_opt的第一个参数是options指针,指向TCP数据帧头部选项字段的首字节地址。第二个参数是TCP数据帧头部选项字段的空间大小。第三个参数是指定添加的选项类型type,第四个参数是选项的value,第五个参数是选项数值占用的内存空间的大小。
Line 13-26: 遍历TCP数据帧头部的选项字段,找到第一个选项type为0的字节地址,存放在popt指针中,做为尚未赋值的TCP选项的首字节地址。遍历过程中计算剩余的选项内存空间left_sz。
Line 27-31: 判断TCP数据帧头部剩余的选项内存空间是否能够存放指定的选项。指定的选项type和length各占一个字节,将add_opt的第五个参数val_sz,选项数值占用的内存空间的大小加2,即得到指定选项总共占用的内存空间。
Line 32-36: 如果指定添加的选项类型opt_type为NOP,则直接将popt的首字节赋值为opt_type,返回NOP选项的长度1即可。NOP选项只有type字段,没有length和value。
Line 37-44: popt指向空闲选项的首字节地址,popt[0]赋值为指定选项的type,popt[1]赋值为指定选项长度,包含type,length和value总共的长度。如果指定选项的value不为空,回顾9.1节,SACK选项只有type和length,没有value字段。如果value不为空,将选项数值拷贝到&popt[2]指向内存空间,返回添加的选项长度,popt[1]。

 static int align_opt(tcphdr_t *tcppkt, unsigned int opt_sz)
 {
     int ret = 0;
     int opt_len = 0, aligned_len = 0;
     unsigned char *popt = NULL;
 
     if (tcppkt == NULL || opt_sz == 0)
         return -1;
     popt = tcppkt->options;
     while (opt_len < opt_sz) {
         if (popt[0] == 0)
             break;
         else if (popt[0] == TCP_OPT_NOP) {
             popt += 1;
             opt_len += 1;
         } else {
             popt += popt[1];
             opt_len += popt[1];
         }
     }
     while (opt_len % 4) {
         ret = add_opt(tcppkt->options, opt_sz,
             TCP_OPT_NOP, NULL, 0);
         if (ret < 0) {
             log_printf(ERROR, "Failed to"
                 "add nop option\n");
             return ret;
         }
         opt_len += ret;
         aligned_len += ret;
     }
     return aligned_len;
 }

align_opt第一个参数为TCP数据帧的首字节地址,第二个参数是TCP数据帧的头部选项空间的大小,用于检查TCP数据帧头部选项的长度是否为4的整数倍,如果不是则,补充添加NOP选项。
Line 9-20: popt指向TCP数据帧选项字段的首字节地址,遍历选项字段,查找类型为0的选项,遍历过程中计算TCP数据帧头部已经有的选项字段的长度,保存在opt_len中。
Line 21-33: 如果opt_len不是4的整数倍,则循环调用add_opt添加NOP选项,直到opt_len为4的整数倍为止。结合process_tcp_syn的调用上下文,synack已经添加了mss,timestamp和window scale选项,共17个字节,此处应循环3次,添加3个NOP选项。
再回到process_tcp_syn函数中,介绍了get_acknum,build_tcp_header和tcp_add_options函数,执行到process_tcp_syn的527行,此时已经为synack数据帧构造了TCP头部,添加了MSS,TIMESTAMP和Window Scale选项,build_tcp_header尚未计算TCP头部的校验和。经过tcp_add_options函数后,synack需要参加校验和计算的所有字段都已填充完毕。调用tcppkt_send,计算并填充TCP头部校验和,将synack发送到IP模块。

 static int tcppkt_send(tcp_conn_t *conn, pdbuf_t *pdbuf)
 {
     int ret = 0;
     tcphdr_t *tcppkt = NULL;
     tcp_pseudo_hdr_t pseudo_hdr;
     unsigned short tcppkt_len = 0;
     tcphdr_flags_t flags;
 
     if (conn == NULL || pdbuf == NULL)
         return -1;
     tcppkt = (tcphdr_t *)pdbuf->payload;
     tcppkt_len = pdbuf->end - pdbuf->payload;
     flags.v = NTOHS(tcppkt->flags_len);
     /* build tcp pesudo header */
     memset(&pseudo_hdr, 0, sizeof(pseudo_hdr));
     memcpy(pseudo_hdr.src_ip, conn->l_ip, sizeof(conn->l_ip));
     memcpy(pseudo_hdr.dst_ip, conn->r_ip, sizeof(conn->r_ip));
     pseudo_hdr.proto = IP_PROTO_TCP;
     pseudo_hdr.len = HSTON(tcppkt_len);
     tcppkt->cksum = HSTON(tcp_cksum(&pseudo_hdr, tcppkt, tcppkt_len));
 
     dump_buf(pdbuf->payload, tcppkt_len);
     ret = ippkt_send(conn->r_ip, IP_PROTO_TCP, pdbuf);
     if (ret)
         goto out;
     if (flags.b.syn)
         conn->send_seq += 1;
     else
         conn->send_seq += (tcppkt_len -
                 (flags.b.hdr_len << 2));
 out:
     return ret;
 }

tcppkt_send定义为TCP模块的静态函数,第一个参数conn是TCP连接的指针,指向与要发送的TCP数据帧对的TCP连接。第二个参数pdbuf存放构造好的TCP数据帧,pdbuf->payload指向待发送的TCP数据帧的首字节地址。成功返回0,失败返回非0。
Line 9-13: 判断conn和pdbuf都不为空时继续执行。通过pdbuf->payload获取TCP数据帧的首字节地址,pdbuf->end - pdbuf->payload得到要发送的TCP数据帧的长度,包括TCP头部和头部后面携带的数据的长度。将TCP数据帧头部的flags_len字段赋值给flags.v,解析头部的flags字段。
Line 14-22: 构造TCP伪头部,conn结构中的源IP地址是DIY TCP/IP的虚拟IP地址,目标IP地址在处理TCP连接三步握手的数据帧时被赋值。回顾9.2节伪头部的定义,TCP Length字段为TCP数据帧的头部和头部后面携带的数据部分的长度的总和,即pdbuf->end - pdbuf->payload。协议字段为IP_PROTO_TCP(0x06),Reserved字段为0。调用tcp_cksum计算TCP头部校验和,参与校验和计算的有TCP伪头部,TCP头部(包括头部选项字段),以及头部后面携带的数据部分。tcp_cksum与9.3节的实现一致,将tcp_cksum返回的校验和转换为大端数值后填充到TCP数据帧的头部校验和字段。dump_buf打印输出即将发送的TCP数据帧的每个字节,用于调试。
Line 23-33: 调用IP模块的发送接口将TCP数据帧发送到IP模块,在IP模块中添加IP头部和以太网头部,再由DIY TCP/IP的网络设备模块调度,通过PF_PACKET类型的socket将数据帧发送到linux kernel的网络设备驱动中。
发送成功时,更新conn结构中本地TCP连接的序号,如果发送的TCP数据帧是TCP SYN则将本地TCP连接的序号加1。如果是携带数据的TCP帧,则将TCP头部的序号conn->send_seq加上TCP数据帧携带的数据的长度。便于描述将TCP头部的序号记为syn_s,数据部分的长度记为len_s,表明序号为syn_s + len_s – 1的字节已经发送完毕,下次要发送的字节的开始序号为syn_s + len_s。如果要发送的数据帧是TCP ACK且没有携带数据,则数据部分的长度为0,conn->send_seq保持不变。本节实现的tcppkt_send并没有考虑数据帧发送失败重传的情况,后续章节继续扩展该函数。
本节的测试与9.5节一致,主机A上运行DIY TCP/IP,指定虚拟IP地址192.168.0.7,和TCP端口号7000,命令行为./tcp_ip_stack -i 192.168.0.7 -p 7000,与主机A处于同一局域网的Android手机主机C上运行iperf client端,命令行为iperf -c 192.168.0.7 -i 1 -t 43200 -p 7000。
Android手机主机C,运行iperf的截屏如下
DIY TCP/IP TCP Connect to DIY TCP/IP
主机C运行iperf TCP客户端,指定TCP Server的IP地址是192.168.0.7,端口号为7000。可以看到主机C的Iperf TCP Client已经与主机C上运行的DIY TCP/IP的TCP Server建立了连接,并开始发送TCP数据帧了。这已经能说明本节实现的代码正确的完成了TCP三步握手的数据帧交互。
同一时间段,运行在主机A上的tcpdump的结果如下:
DIY TCP/IP TCP Dump on Device A
主机A上tcpdump捕获eth0网络接口的数据帧,与DIY TCP/IP中pcap_open_live指定的网络接口一致。-s 0指定存放数据包的缓存为最大值65525,-w指定将捕获的数据帧存入tcp_conn.pcap文件。从tcpdump的结果来看,一共捕获到了13个数据帧。
同一时间段,运行在主机A上的DIY TCP/IP的结果如下:
DIY TCP/IP 3-WAY Handshake success
从运行结果可以看出,DIY TCP/IP的TCP模块先收到了TCP SYN数据帧,连接状态从LISTEN转换到SYN-RECEIVED,回复了TCP SYN-ACK共40个字节,然后又收到了主机C上iperf TCP客户端发出的TCP ACK,携带的数据长度为0,连接状态从SYN-RECEIVED转换到ESTABLISHED,完成了TCP三步握手数据帧的交互,建立了TCP连接。紧接着TCP模块又打印出两次7000 port not available,3次invalid tcp checksum。说明TCP连接建立后又收到了5个TCP数据帧。回顾本节tcppkt_recv函数的实现,首先检验TCP头部校验和,检验通过后,根据目标端口查找已经建立的TCP连接,这部分内容是注释尚未实现。在没有找到与目标端口对应的已经建立的TCP连接的情况下,查找处于监听状态的TCP连接,因此可以判断连接状态进入ESTABLISHED之后,先收到了2个TCP数据帧,并且检验通过了TCP头部校验和,查找与目标端口对应的处于监听状态的TCP连接失败,所以打印出7000 port not available,这个打印是合理的。
但是没有通过头部校验和和检验的3次invalid tcp checksum的打印是不合理的,按照我们的分析,连接状态转换到ESTABLISHED之后,收到的TCP packet都应该能通过头部校验和检验,并打印出7000 port not available。通过wireshark打开主机A上tcpdump的抓包结果,继续分析。
主机A上tcpdump的结果在wireshark中打开如下
DIY TCP/IP tcpdump on device A
从wireshark的分析中可以看到,主机C(192.168.0.111)首先发出了ARP Reuqest,然后主机A上运行的的DIY TCP/IP正确回复了ARP Reply,建立了虚拟IP地址192.168.0.7与eth0硬件地址的映射。主机C又发出了TCP SYN数据帧,然后主机A回复了TCP SYN-ACK,主机C再回复TCP ACK,完成三步握手。TCP连接建立之后,主机A收到了一个长度为90和一个长度为1514的TCP数据帧,对应DIY TCP/IP的”7000 port not available”的打印输出,后面三个TCP数据帧的长度均超过1514,回顾8.6节IP分片的接收,明显的可以看出,这3个TCP数据帧的长度已经超过了MTU 1500字节,主机C上的IP层和TCP层均没有将其分片处理。这是不合理的,这里先指出,如果通过空中抓包捕获主机C发出的TCP数据帧,长度均未超出1500字节,但主机A收到的TCP数据帧却超过了1500,这个与主机A上Linux kernel的GRO (Generic Receive Offload)有关。因此DIY TCP/IP收到的3个长度超过1514的TCP数据帧,GRO是能的情况下DIY TCP/IP收到的TCP数据帧校验和均检验失败,是合理的,有关GRO对PF_PACKET数据帧接收的影响,在9.8节介绍。
分析了DIY TCP/IP的打印输出后,得知TCP连接建立后的5个异常打印都是合理的,继续分析TCP三步握手的数据帧。
展开三步握手的第一个TCP SYN数据帧,TCP SYN由主机C (192.168.0.111)发出。
DIY TCP/IP TCP SYN
展开后的TCP SYN数据帧的头部可以看到,源端口是49475,目标端口是DIY TCP/IP指定监听的7000。sequence number 为0,后面括号中的内容是relative sequence number,这是wireshark软件解析的结果,说明该序号是相对值。在右边的raw byte显示栏可以看到sequence number的值为0x90279dfc(2418515452十进制), TCP数据帧携带的数据长度为0。
展开三步握手的第二个TCP SYN-ACK数据帧,TCP SYN-ACK由DIY TCP/IP发出。
DIY TCP/IP TCP SYN-ACK
查看TCP SYN-ACK的头部可以看到,源端口和目标端口分别是7000和49475,与TCP SYN数据帧中的目标端口和源端口对应。Sequence number是0,DIY TCP/IP构造TCP SYN-ACK时,将TCP连接的本地端TCP的序号设置为0。Acknowlege number是0x90279dfd,是TCP SYN的Sequence number加1,与process_tcp_syn中的处理一致。在看flags字段为0x12,bit1和bit4置位,再看header length字段为40,回顾process_tcp_syn的代码,构造synack数据帧的长度是40,只有TCP头部,头部后面不携带任何数据,最后看TCP选项字段。process_tcp_syn通过选项位图设置添加了3个TCP选项,分别是MSS,Timestamp和window scale,这三个TCP选项的数值与tcp_add_options的代码实现一致,最后是三个NOP选项,将选项字段补充为4的整数倍。与本节的代码实现完全一致。
展开第三帧TCP ACK,TCP ACK由主机C (192.168.0.111)发出。
DIY TCP/IP TCP ACK
主机C能回复TCP ACK,说明本节构造的TCP SYN-ACK是正确的,头部校验和的计算也是正确的。重点看TCP ACK的sequence number字段,是SYN-ACK的sequence number加1,即1。与TCP SYN-ACK设置的TCP的开始序号0对应。
通过wireshark对tcpdump结果的分析,得知本节实现的代码是正确的,完成了TCP三步握手数据帧的交互。
下一篇:DIY TCP/IP TCP模块的实现6

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值