上一篇:DIY TCP/IP IP模块和ICMP模块的实现2
8.5 IP数据帧的发送
本节为构造好的ICMP (Ping) Reply添加IP头部,根据目标IP地址查找ARP模块的ARP表,在IP头部前面再添加以太网头部,最终通过DIY TCP/IP的网络设备模块把Echo (Ping) Reply发送到对方主机。本章一直沿用的测试都是在与运行DIY TCP/IP的主机A,处于同一局域网的另外一台主机上B上,通过PING DIY TCP/IP的虚拟IP地址测试IP数据帧的接收,ICMP数据帧的接收,但在主机B上看到的PING的结果都是失败的,本节结束时朋友们将会看到主机B能成功PING通DIY TCP/IP的虚拟IP地址(局域网中不存在的IP地址)。
ippkt_send函数是IP模块暴露给其他模块使用的接口函数,函数声明在ip.h中
#ifndef _IP_H_
#define _IP_H_
…
int ippkt_recv(unsigned char *pkt, unsigned int sz);
int ippkt_send(unsigned char *dst, unsigned char proto, void *buf);
#endif
Line 5: ippkt_send函数声明,第一个参数dst指针,指向目标IP地址,第二个参数是协议类型,例如ICMP模块调用ippkt_send发送ICMP (Ping) Reply数据帧,proto的值就是0x1(IP_PROTO_ICMP),用于构造IP头部中的上层协议类型。第三个参数实际上是pdbuf_t *类型的指针,指向存放上层协议数据的pdbuf,定义为void *是为避免在ip.h中引用pdbuf.h,造成头文件的循环引用,ippkt_send成功返回0,出错返回非0值。
ippkt_send函数实现在ip.c中
int ippkt_send(unsigned char *dst, unsigned char proto, void *buf)
{
int ret = 0;
pdbuf_t *pdbuf = (pdbuf_t *)buf;
if (dst == NULL || pdbuf == NULL) {
ret = -1;
goto out;
}
if ((ret = build_ip_header(dst, proto, pdbuf, 0))) {
log_printf(ERROR, "failed to build ip header\n");
goto out;
}
netdev_tx_pkt(pdbuf);
out:
if (ret && pdbuf)
pdbuf_free(pdbuf);
return ret;
}
Line 1-14: 将buf强制转换为pdbuf_t *类型,判断dst和pdbuf均不为空时,继续执行,调用build_ip_header为上层协议数据添加IP头部,在IP头部的前面再添加以太网头部。
Line 15-21: pdbuf构造完成之后,就是具备各层协议头部的完整数据帧,通过netdev_tx_pkt函数将数据帧放入网络设备模块的发送队列,等待发送。如果build_ip_header返回出错,则调用pdbuf_free释放pdbuf。
build_ip_headr完成了构造IP头部的工作,该函数是IP模块内的新增静态函数,只在IP模块内部使用,先来看该函数的入参和返回值。
static int build_ip_header(unsigned char *dst, unsigned char proto,
pdbuf_t *pdbuf, unsigned short flags_offset)
{
build_ip_header的前三个参数都是ippkt_send函数传入的,未经修改,含义和值与ippkt_send的三个参数一致。第四个参数是IP头部的flags和分片偏移,这个参数在IP分片的发送一节介绍。本节发送的IP数据帧的数据部分是ICMP (Ping) Reply,长度都不超过MTU 1500字节,本节中flags_offset的值为0。
static int build_ip_header(unsigned char *dst, unsigned char proto,
pdbuf_t *pdbuf, unsigned short flags_offset)
{
int ret = 0;
iphdr_t *iphdr = NULL;
unsigned char *local_ip = NULL;
unsigned short payload_len = 0;
local_ip = netdev_ipaddr();
if (local_ip == NULL) {
ret = -1;
goto out;
}
payload_len = (unsigned short)(pdbuf->end - pdbuf->payload);
/* build ip header */
pdbuf_push(pdbuf, sizeof(iphdr_t));
iphdr = (iphdr_t *)pdbuf->payload;
iphdr->hdr_len = IP_HDR_LEN;
iphdr->ver = IP_VERSION;
iphdr->tos = 0;
iphdr->total_len = HSTON(payload_len + sizeof(iphdr_t));
iphdr->id = get_random(&iphdr->id, sizeof(iphdr->id));
iphdr->id = HSTON(iphdr->id);
iphdr->flags_offset = HSTON(flags_offset);
iphdr->ttl = 64;
iphdr->proto = proto;
iphdr->hdr_cksum = 0;
memcpy(iphdr->src_ip, local_ip, sizeof(iphdr->src_ip));
memcpy(iphdr->dst_ip, dst, sizeof(iphdr->dst_ip));
iphdr->hdr_cksum = cksum(0, iphdr, sizeof(iphdr_t), BENDIAN);
iphdr->hdr_cksum = HSTON(iphdr->hdr_cksum);
dump_buf(iphdr, sizeof(iphdr_t));
ret = build_ethernet_header(pdbuf, ETHERNET_IP,
local_ip, iphdr->dst_ip,
NULL, NULL);
out:
return ret;
}
Line 1-15: 调用网络设备模块的netdev_ipaddr获取DIY TCP/IP的虚拟IP地址。pdbuf是ICMP模块构造好的ICMP Echo (Reply)数据帧,pdbuf->paylad指向ICMP头部首字节地址处,pdbuf->end 与pdbuf->payload的差值是ICMP头部和ICMP Echo Reply数据部分的总长度。
Line 16-21: pdbuf_push将pdbuf->payload向地址减小的方向移动sizeof(iphdr_t)个字节,为IP头部预留空间,将pdbuf->payload强制转换为iphdr_t *类型,接下来的代码填充IP头部各个字段值。hdr_len是固定长度5,代表IP头部有5个四字节数据,共20字节。ver字段也是固定数值4,目前DIY TCP/IP只支持IPv4,hdr_len和ver共占用一个字节,不用进行小端到大端的转换。tos字段为0,IP模块暂不实现DSCP和ECN的内容。
Line 22-23: total_len为ICMP Echo (Ping) Reply的长度加上IP首部的长度,再转换成大端格式。Id是用来标识IP分片的一个16-bit数值,当其他模块发送的数据超过MTU 1500字节时,IP层需要将数据分片,属于同一IP帧的分片的id相同。本节发送的ICMP Echo Reply不用进行分片,只需给id赋一个随机值即可。本人尝试过将id的初始值设为0,每发一个IP数据帧递增该数值,这种情况接收端不接收IP数据帧,设为随机值后IP数据帧可以被正确接收。get_random是utils.c模块的新增函数,介绍完该函数后再来看它的实现。获取16-bit的随机值做为id后,将其转换为大端格式。
Line 24-28: flags_offset是buid_ip_header的入参,目前为0,在介绍IP分片的发送时,flags字段的more fragment和分片偏移都将会有具体的数值,flags_offset为16-bit数值,需转换为大端格式。ttl (time to live)表明ICMP Echo (Ping) Reply数据帧可以经过的路由器跳数。DIY TCP/IP只在局域网内运行,设置为64即可。proto是上层协议类型,是build_ip_header的入参,长度为8-bit,直接赋值即可。cksum字段先赋值为0,等IP头部的各个字段都填充好后,再来计算IP首部的校验和。
Line 29-33: IP头部的源IP地址是DIY TCP/IP的虚拟IP地址,目标IP地址是build_ip_header的入参,该目标IP地址通过接收到的对应的IP数据帧的源IP地址获得。对于ICMP Echo (Ping) Reply来说,目标IP地址就是封装ICMP Echo (Ping) Request的IP数据帧的源IP地址。调用cksum计算IP首部的校验和,参与校验和计算的数据是20个字节的IP首部数据,计算完成后将校验和转换为大端格式。dump_buf做为debug使用,查看构造的IP首部是否符合预期。
Line 34-39: 调用ARP模块的build_ethernet_header函数,构造以太网首部数据。build_ethernet_header查找ARP表可以获取目标IP地址对应的硬件地址。build_ehternet_header在7.3节已将详细介绍过。build_ethernet_header成功时返回0,失败时返回非0值。
get_random的实现在utils.c中
int get_random(void *buf, unsigned int sz)
{
int ret = 0;
FILE *fp = NULL;
if (buf == NULL || sz == 0)
return -1;
fp = fopen("/dev/urandom", "r");
if (fp == NULL) {
printf("Failed to open /dev/urandom, %s (%d)\n", strerror(errno), errno);
return -1;
}
ret = fread(buf, 1, sz, fp);
if (ret < sz)
ret = -1;
if (fp)
fclose(fp);
return ret;
}
get_random通过Linux kernel提供的伪随机数设备节点/dev/urandom来读取指定长度的伪随机数,urandom不依赖系统中断,也不会造成进程阻塞。通过文件操作函数fopen打开设备节点获取文件指针fp后,通过fread读取sz个字节的伪随机数,存放在buf指向的内存空间,返回给调用者。
到这里ippkt_send,完成了IP首部构造,在IP首部前添加了以太网首部,可以向网络设备模块发送数据帧了。在process_icmp_echo函数中添加对ippkt_send的调用。
static int process_icmp_echo(iphdr_t *ippkt, icmphdr_t *echo_req)
{
…
//dump_buf(pdbuf->payload, pdbuf->end - pdbuf->payload);
ret = ippkt_send(ippkt->src_ip, IP_PROTO_ICMP, pdbuf);
out:
return ret;
}
line 5添加ippkt_send调用,将接收到的IP数据帧的源IP地址做为目标IP地址,上层协议类型为IP_PROTO_ICMP,pdbuf存放构造好的ICMP (Ping) Echo Reply数据帧。process_icmp_echo在8.4节已经介绍过,也已经看过运行结果,构造的ICMP Echo (Ping) Reply数据帧符合预期,本节集中在IP首部的构造,将ICMP模块的dump_buf都注释掉,关注dump_buf对IP首部的打印信息。
测试方法本节开始处已经介绍过,主机A运行DIY TCP/IP之前,先来PING DIY TCP/IP的虚拟IP地址192.168.0.7,确保该地址在局域网中不存在。主机B的运行结果如下:
确保192.168.0.7在局域网中不存在后,主机A运行DIY TCP/IP,主机B此时再次PING 该IP地址,运行结果如下:
看到DIY TCP/IP的虚拟IP地址已经被成功PING通了,通过主机B的运行结果可以看到,主机B发出的4个ICMP Echo (Ping) Reply均收到来自192.168.0.7的回复,来看主机A上的运行结果
gannicus@ubuntu:~/guojia/tasks/DIY_USER_SPACE_TCPIP/ch5/3$ sudo ./tcp_ip_stack -i 192.168.0.7
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
45 00 00 3c 2e 40 00 00 40 01 ca c0 c0 a8 00 69
c0 a8 00 07
Echo (ping) request, id: 1, seq: 5/1280, ttl=64
Echo (ping) reply, id: 1, seq: 5/1280, ttl=64
45 00 00 3c 2e 41 00 00 40 01 ca bf c0 a8 00 69
c0 a8 00 07
Echo (ping) request, id: 1, seq: 6/1536, ttl=64
Echo (ping) reply, id: 1, seq: 6/1536, ttl=64
45 00 00 3c 2e 42 00 00 40 01 ca be c0 a8 00 69
c0 a8 00 07
Echo (ping) request, id: 1, seq: 7/1792, ttl=64
Echo (ping) reply, id: 1, seq: 7/1792, ttl=64
45 00 00 3c 2e 45 00 00 40 01 ca bb c0 a8 00 69
c0 a8 00 07
Echo (ping) request, id: 1, seq: 8/2048, ttl=64
Echo (ping) reply, id: 1, seq: 8/2048, ttl=64
^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: 5, Free: 5
DIY TCP/IP收到4个ICMP Echo (Ping) Request,sequence 分别是5,6,7,8。Ippkt_send构造的IP header是20个字节,dump_buf打印出的IP头部数据也是20个字节。来看Line11-12行打印的IP首部数据,第1个字节0x45,低四位是hdr_len 5,高四位是version 4;第2个字节tos为0x0;第3,4个字节为total_len,0x00,0x3c,0x3c为16-bit的低字节,转换为10进制为60,主机B发出的Echo Ping Request的数据长度为32,加上8个字节的ICMP首部和,20个字节的IP首部,刚好是60个字节。第5,6个字节为id,0x2e,0x40;第7,8个字节为flags_offset 0x00,0x00。第9个字节0x40是ttl,十进制为64,第10个字节是ICMP协议类型0x01。第11,12个字节为IP头部校验和;第13,14,15,16个字节为目标IP地址0xc0,0xa8,0x00,0x69 (192 168 0 105),第17,18,19,20个字节为源IP地址0xc0,0xa8,0x00,0x07(192 168 0 7)。后面3个IP首部不再一一介绍,结束运行后pdbuf模块的统计数据显示,一共申请了5个pdbuf,其中4个为ICMP Reply,一个是ARP Reply,没有内存泄漏。
下节内容:DIY TCP/IP IP模块和ICMP模块的实现4