DIY TCP/IP IP模块和ICMP模块的实现3

上一篇: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的运行结果如下:
DIY TCP/IP Ping Fail Sample
确保192.168.0.7在局域网中不存在后,主机A运行DIY TCP/IP,主机B此时再次PING 该IP地址,运行结果如下:
DIY TCP/IP Ping Sample
看到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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值