上一篇: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头部校验和:
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