上一篇: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
主机C是笔者的Android手机,运行iperf的截屏如下
主机C运行iperf TCP客户端,指定TCP Server的IP地址是192.168.17,iperf TCP客户端默认TCP server端的port为5001。iperf客户端没有打印任何Log,说明该IP地址在局域网中不存在。
运行在主机A上的tcpdump的结果如下:
主机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首先是接收到了来自主机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的运行结果。
从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的打印输出一致。
展开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