上一篇: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); #endifLine 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: x, "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 <string.h> #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头部校验和:
将20字节的IP头部每两个字节组成一个16位无符号整型值,校验和字段也包括在内,进行两两二进制相加 。最高位有进位时,将进位加到累加结果的最低位。最后将累加结果取反,取反后的结果为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,运行结果如下: 上图可以看到主机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
