DIY TCP/IP ARP模块3

上一篇:DIY TCP/IP ARP模块2
7.4 ARP表的实现
先来概括介绍下ARP表,和ARP表的查找过程。ARP表存放局域网中IP地址和硬件地址的映射,构造以太网头部时需要根据目标IP地址查找ARP表获取目标硬件地址。如果目标IP地址与网络接口的IP地址在同一个局域网中,则查找ARP表,获取对应的目标硬件地址,如果查找失败则发送ARP Request,获取目标IP地址对应的硬件地址。收到ARP Reply后更新ARP表项。如果目标IP地址与网络接口的IP地址不在同一个局域网中,则查找路由表,得到下一跳网络接口IP地址,再查找下一跳的IP地址对应的硬件地址,构造相应的以太网头部。
DIY TCP/IP的ARP表的实现相对简化,由于DIY TCP/IP只运行在局域网中,所以暂不实现目标IP地址与网络接口IP地址不在同一网段的处理。另外ARP表的更新,不是通过先发送ARP Request,在得到ARP Reply之后,更新ARP表。而是根据ARP Requset更新ARP表,当DIY TCP/IP的ARP模块收到ARP Request数据帧后,判断请求的IP地址和虚拟IP地址相等时,就将ARP Request数据帧中的源IP地址和源硬件地址做为ARP表项,更新到ARP表中,简化查表失败时先发送ARP Reuqest再根据ARP Reply更新ARP表的过程。
本节的目标是改写7.1.3节中的build_ethernet_header函数,该函数暴露给其他模块使用,构造以太网头部。7.1.3节是在已知源硬件地址和目标硬件地址的情况下调用该函数,所以没有涉及ARP表的查找操作。围绕该函数的改写,引入ARP表项数据结构的定义,以及在该数据结构的基础上实现的ARP表,表项的新增,删除和查找操作。
先在arp.c中增加ARP表项数据结构的定义

  typedef struct _arp_entry {
      unsigned char ip[ARP_PROTO_SZ];
      unsigned char mac[ARP_HW_SZ];
  } arp_entry_t;
  
  static arp_entry_t *arp_table = NULL;
  static unsigned int num_arp_entry = 0;
  static arp_entry_t null_entry = {
      .ip = {0, 0, 0, 0},
      .mac = {0, 0, 0, 0, 0, 0},
  };

Line 1-4: arp_entry_t结构体包括两个固定长度的unsigned char数组,ip长度为4个字节,mac长度为6个字节,分别用于存放ARP表项的IP地址和对应的硬件地址。
Line 6-7: arp_entry_t类型的指针,arp_table指向的内存存放ARP表,num_arp_entry是ARP表项数目。
Line 8-11: null_entry是arp_entry_t类型,ip和mac均初始化为0,用于匹配ARP表中的空闲表项。
围绕arp_entry_t数据结构的定义,实现ARP表的初始化,销毁,表项的新增和删除函数。

  static int init_arp_table()
  {
      int ret = 0;
      if (arp_table != NULL)
          goto out;
      arp_table = (arp_entry_t *)malloc(sizeof(arp_entry_t));
      if (arp_table == NULL) {
          log_printf(ERROR, "No memory for arp table, %s (%d\n",
                  strerror(errno), errno);
          ret = -1;
          goto out;
      }
      memset(arp_table, 0, sizeof(arp_entry_t));
      num_arp_entry = 1;
  out:
      return ret;
  }
  
  static void deinit_arp_table()
  {
      if (arp_table == NULL)
          return;
      log_printf(INFO, "Destroy ARP table, %u %s\n",
          num_arp_entry, num_arp_entry > 1 ? "entries" : "entry");
      free(arp_table);
  }

Line 1-17: init_arp_table函数初始化ARP表, arp_table指针为空时,通过malloc申请能够存放一个ARP表项的内存空间,arp_table指向该内存空间的首地址。malloc执行失败时,打印出错信息,结束执行。malloc成功时,初始化arp_table指向的内存空间为全0,num_arp_entry为1。
Line 19-26: deinit_arp_table,销毁ARP表,打印ARP表中的表项数目,然后调用free释放arp_table占用的内存空间。
init_arp_table和deinit_arp_table是ARP模块的静态函数,分别在ARP模块的初始化函数arp_init和销毁函数arp_deinit中被调用。

  static void dump_arp_table()
  {
      unsigned int i = 0;
      log_printf(INFO, "ARP Table\n");
      log_printf(INFO, "IP Address\t\tMAC Address\n");
      for (i = 0; i < num_arp_entry; i ++) {
          if (memcmp(&arp_table[i], &null_entry, sizeof(arp_entry_t)) == 0)
              continue;
          log_printf(INFO, IPSTR"\t\t"MACSTR"\n",
                  IP2STR(arp_table[i].ip),
                  MAC2STR(arp_table[i].mac));
      }
  }

Line 1-13: dump_arp_table遍历ARP表,打印ARP表项的内容,跳过内容为全0的null_entry表项。

  static int add_arp_entry(unsigned char *ip, unsigned char *mac)
  {
      int ret = 0;
      unsigned int i = 0;
      arp_entry_t tmp_entry;
  
      memcpy(tmp_entry.ip, ip, ARP_PROTO_SZ);
      memcpy(tmp_entry.mac, mac, ARP_HW_SZ);
      for (i = 0; i < num_arp_entry; i ++) {
          if (memcmp(&tmp_entry, &arp_table[i], sizeof(arp_entry_t)) == 0) {
              log_printf(INFO, "ARP entry: "IPSTR" "MACSTR" already exists\n",
                  IP2STR(ip), MAC2STR(mac));
              goto out;
          }
      }
      /* look for null entry */
      for (i = 0; i < num_arp_entry; i ++)
          if (memcmp(&arp_table[i], &null_entry, sizeof(arp_entry_t)) == 0)
              break;
      /* extend arp table */
      if (i >= num_arp_entry) {
          arp_table = realloc(arp_table, (num_arp_entry + 1) * sizeof(arp_entry_t));
          if (arp_table == NULL) {
              log_printf(ERROR, "No memory for new arp entry, %s (%d)\n",
                  strerror(errno), errno);
              ret = -1;
              goto out;
          }
          memset(&arp_table[i], 0, sizeof(arp_entry_t));
          num_arp_entry += 1;
      }
      memcpy(&arp_tabe[i], &tmp_entry, sizeof(arp_entry_t));
  out:
      return ret;
  }

add_arp_entry新增ARP表项,入参为IP地址和硬件地址的指针。定义局部变量tmp_entry,通过入参ip和mac初始化tmp_entry,这样在查找ARP表做比较时可以用ARP表项为单位比较。遍历ARP表,如果tmp_entry已经在ARP表中,返回ret=0。如果没有找到tmp_entry,则tmp_entry做为新增ARP表项加入ARP表中,首先在ARP表中查找null_entry表项,如果找到,则将temp_entry复制到null_entry的位置。如果没有找到null_entry表项,则通过realloc扩展arp_table的内存空间。realloc扩展arp_table失败时,打印提示信息,返回ret=-1,成功时扩展的内存空间大小为一个ARP表项。将扩展的内存空间清0后,累加num_arp_entry,最后将temp_entry复制到新扩展的表项位置,完成ARP表项的添加。

 static int del_arp_entry(unsigned char *ip, unsigned char *mac)
 {
     int ret = -1;
     unsigned int i = 0;
     for (i = 0; i < num_arp_entry; i ++) {
         if (memcmp(ip, arp_table[i].ip, ARP_PROTO_SZ) == 0 &&
         memcmp(mac, arp_table[i].mac, ARP_HW_SZ) == 0) {
             log_printf(INFO, "Delete ARP entry: "IPSTR" "MACSTR"\n",
             IP2STR(ip), MAC2STR(mac));
             memset(&arp_table[i], 0, sizeof(arp_entry_t));
             ret = 0;
             break;
         }
     }
     return ret;
 }

del_arp_entry,删除ARP表项,入参为IP地址和硬件地址的指针。遍历arp_table的每个表项,比较ip地址和硬件地址和入参IP和硬件地址相等的表项,找到对应表项时将其置为null_entry。

 static int update_arp_table(unsigned char *ip, unsigned char *mac, unsigned int add)
 {
     int ret = 0;
     if (ip == NULL || mac == NULL) {
         log_printf(ERROR, "Invalid params to update ARP table\n");
         return -1;
     }
     if (add)
         ret = add_arp_entry(ip, mac);
     else
         ret = del_arp_entry(ip, mac);
     dump_arp_table();
     return ret;
 }

update_arp_table,更新ARP表,入参为IP地址和硬件地址的指针,add标记更新操作是添加还是删除操作。检查ip和mac入参均不为空时,如果add不为0,则将调用arp_add_entry添加新的ARP表项,如果add为0,则调用add_del_entry删除表项。函数末尾调用dump_arp_table打印更新后的arp表,做为debug使用。

static unsigned char *lookup_arp_table(unsigned char *ip)
 {
     unsigned int i = 0;
     unsigned char *mac = NULL;
     unsigned char *local_ip = NULL;
     /* hardcode subnet mask 255.255.255.0 */
     unsigned char subnet_mask[4] = {0xff, 0xff, 0xff, 0x0};
 
     if (ip == NULL)
         goto out;
     local_ip = netdev_ipaddr();
     if (local_ip == NULL)
         goto out;
     /* within same subnet */
     if (((ip[0] & subnet_mask[0]) == local_ip[0]) &&
         ((ip[1] & subnet_mask[1]) == local_ip[1]) &&
         ((ip[2] & subnet_mask[2]) == local_ip[2])) {
         for (i = 0; i < num_arp_entry; i ++) {
             if (memcmp(ip, arp_table[i].ip, ARP_PROTO_SZ) == 0) {
                 mac = arp_table[i].mac;
                 break;
             }
         }
         /* no arp entry found, send out arp request*/
         if (i >= num_arp_entry) {
             /* todo send out arp request */
         }
     } else {
         /* lookup route table for next hop ip address */
         log_printf(WARNING, "Need to lookup route table\n");
     }
 out:
     return mac;
 }

lookup_arp_table查找ARP表,本节开始时概括介绍过ARP的查表过程,现在来看其具体实现。
lookup_arp_table的入参为IP地址的指针,返回值是unsigned char *指针,返回null时查表失败,成功时返回值指向ARP表中与入参IP地址对应的硬件地址。
Line 3-7: 定于局部变量i,mac初始值为null,local_ip是DIY TCP/IP中pcap_t设备的网络接口ip地址,subnet_mask子网掩码,被硬编码为255.255.255.0。用于比较入参IP地址和local_ip地址的网络前缀是否相等。
Line 9-13: 判断入参IP是否为空,以及调用网络设备模块的netdev_ipaddr获取网络接口的IP地址的指针,赋值给local_ip。
Line 14-34: 首先比较入参IP和local_ip的24位的网络前缀是否相等,判断入参IP地址和local_ip是否在相同局域网中。如果不再同一局域网中,则需要查找路由表,获取下一跳网络接口的IP地址,该部分功能暂不实现。如果在同一局域网中,则遍历arp_table,查找与入参IP相等的ARP表项,查找成功时将ARP表项中的硬件地址的指针赋值给mac,查找失败时需要发送ARP Request获取与入参IP对应的硬件地址,该部分内存简化实现为通过接收ARP Request更新ARP表项。暂时不实现的内容和简化实现的内容本节开始已经介绍过原因,该函数最后返回mac指针的值。
以上内容介绍了围绕arp_entry_t数据结构的定义,实现的init_arp_table,deinit_arp_table,dump_arp_table,add_arp_entry,del_arp_entry,update_arp_table和lookup_arp_table。这些函数都是ARP模块内部的静态函数,仅在ARP模块内部使用。
先来看update_arp_table在ARP模块内部的使用,本节开头部分已经介绍了ARP表的更新不仅仅依赖于收到的ARP Reply数据帧,当收到ARP Request数据帧,判断需要回复ARP Reply数据帧时,同样更新ARP表,简化ARP表失查找败时,发送ARP Request的处理。所以要修改proces_arp_request函数,加入update_arp_table的调用,更新ARP表。

 static int process_arp_request(arphdr_t *pkt)
 {
…
     if (memcmp(pkt->target_ip, ipaddr, ARP_PROTO_SZ))
         goto out;
     log_printf(INFO, IPSTR " is at "MACSTR"\n",
         IP2STR(pkt->target_ip), MAC2STR(hwaddr));
     update_arp_table(pkt->sender_ip, pkt->sender_mac, 1);
     /* build ARP reply */
     pdbuf = pdbuf_alloc(ARP_PADDING_SZ, 0);
     if (pdbuf == NULL) {
         ret = -1;
         goto out;
     }
     pdbuf_push(pdbuf, sizeof(arphdr_t) + ARP_PADDING_SZ);
     reply = (arphdr_t *)pdbuf->payload;
     memcpy(reply, pkt, sizeof(arphdr_t));
     reply->op_code = HSTON(ARP_REPLY);
     memcpy(reply->sender_mac, hwaddr, ARP_HW_SZ);
     memcpy(reply->sender_ip, ipaddr, ARP_PROTO_SZ);
     memcpy(reply->target_mac, pkt->sender_mac, ARP_HW_SZ);
     memcpy(reply->target_ip, pkt->sender_ip, ARP_PROTO_SZ);
     build_ethernet_header(pdbuf, ETHERNET_ARP,
             reply->sender_ip, reply->target_ip,
             reply->sender_mac, reply->target_mac);
     netdev_tx_pkt(pdbuf);
 out:
     return ret;
 }

Line 8: 判断ARP Reuquest请求的IP地址和虚拟IP地址相等后,构建ARP Reply数据帧之前,调用update_arp_table,将发送ARP Reply数据帧的源IP地址和源硬件地址,加入ARP表中,更新ARP表。process_arp_reuquest其余部分的代码与7.1.3节一致,不再细述。
再来看ARP模块暴露给其他模块使用的新增函数:arp_init,arp_deinit,分别调用arp_init_table和arp_deinit_table完成ARP表的初始化,和ARP表的销毁操作。

 int arp_init()
 {
     return init_arp_table();
 }
 
 void arp_deinit()
 {
     deinit_arp_table();
 }

再来看ARP模块暴露给其他模块使用的一个重要函数,build_ethernet_header,当其他模块调用该函数的入参dst_mac,目标硬件地址为空时,增加查找ARP表的过程。

 static int build_ethernet_header(void *buf, unsigned short type,
             unsigned char *src_ip, unsigned char *dst_ip,
             unsigned char *src_mac, unsigned char *dst_mac)
 {
     int ret = 0;
     pdbuf_t *pdbuf = NULL;
     ethhdr_t *ethhdr = NULL;
 
     if (buf == NULL || (dst_ip == NULL && dst_mac == NULL )) {
         log_printf(ERROR, "Invalid parameters to build ethernet header\n");
         ret = -1;
         goto out;
     }
 
     if (src_mac && dst_mac)
         goto build_hdr;
     if (src_mac == NULL)
         src_mac = netdev_hwaddr();
     if (dst_mac == NULL)
         dst_mac = lookup_arp_table(dst_ip);
     if (dst_mac == NULL || src_mac == NULL) {
         ret = -1;
         goto out;
     }
 build_hdr:
     pdbuf = buf;
     pdbuf_push(pdbuf, sizeof(ethhdr_t));
     ethhdr = (ethhdr_t *)pdbuf->payload;
     memcpy(ethhdr->dst, dst_mac, sizeof(ethhdr->dst));
     memcpy(ethhdr->src, src_mac, sizeof(ethhdr->src));
     ethhdr->type = HSTON(type);
     log_printf(VERBOSE, "eth hdr, dst: "MACSTR" src: "MACSTR" type: %04x\n",
         MAC2STR(ethhdr->dst), MAC2STR(ethhdr->src), NTOHS(ethhdr->type));
 out:
     return ret;
 }

高亮部分的代码是本节对build_ethernet_header函数做的修改,其余部分与7.1.3节的实现一致。高亮部分首先判断build_ether_header的入参,源硬件地址src_mac和目标硬件地址dst_mac是否都不为空,如果都不为空则 直接跳转到build_hdr标号处构建以太网头部。如果源硬件地址为空,则调用网络设备模块的函数netdev_hwaddr获取网络接口硬件地址,如果目标硬件地址为空,则调用lookup_arp_table根据目标IP地址查找对应的目标硬件地址。成功获取源硬件地址和目标硬件地址的条件下,构建以太网头部数据。line188行判断如果src_mac和dst_mac任意一个为空,则返回ret=-1。
build_ethernet_header与arp_init,arp_deinit一样,都是ARP模块暴露给其他模块使用的函数接口,build_ethernet_header将在IP模块发送数据帧时被调用,arp_init和arp_deinit在DIY TCP/IP的初始化代码中被调用,来看init.c中main函数的修改。

 int main(int argc, char *argv[])
 {
…
 skip_option:
  signal(SIGINT, signal_handler);
 
  ndev = netdev_init(DEFAULT_IFNAME);
  if (ndev == NULL) {
      ret = -1;
      goto out;
  }
  if (ip_cfg)
      netdev_set_ipaddr(ndev, fake_ip, sizeof(fake_ip));
 
  pdbuf_init();
  arp_init();
  netdev_start_loop(ndev);
 out:
  if (ndev)
      netdev_deinit(ndev);
  arp_deinit();
  pdbuf_deinit();
  return ret;
 } 

DIY TCP/IP的初始化代码,init.c中的main函数,已经被多次修改过,随着各个模块的实现,加入了模块的初始化代码和销毁代码的调用。最近一次修改是在7.2节,加入了main函数入参解析的实现。本节在pdbuf模块的初始化之后,加入arp_init,对应的在pdbuf_deinit模块销毁之前加入arp_deinit的调用。pdbuf模块管理DIY TCP/IP其他模块对buffer的分配和释放,pdbuf模块被销毁时将统计buffer分配的总数和释放的总数,提示有无内存泄漏,所以应当是销毁协议栈时最后一个被销毁的模块。
编译运行结果如下:

 gannicus@ubuntu:~/guojia/tasks/DIY_USER_SPACE_TCPIP/ch4/3$ 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.107        8c:a9:82:11:d1:de
 ^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

本节的测试方法与7.1.3节一样,设置局域网中不存存在的IP地址192.168.0.7为DIY TCP/IP的虚拟IP地址,在同一局域网中的另外一台win7机器上ping 192.168.0.7,DIY TCP/IP收到ARP Request请求后,判断请求IP地址与虚拟IP地址相等,打印出192.168.0.7 is at 00:0c:29:2e:0a:ed,紧接着调用update_arp_table更新ARP表,update_arp_table函数的末尾调用dump_arp_table,打印ARP表的内容。输入ctrl+c,结束DIY TCP/IP运行时,arp_deinit释放ARP表占用的内存,并统计ARP表中表项的数目为1。
7.5 小结
本章介绍了DIY TCP/IP的ARP模块的实现,包括ARP数据帧的结构,ARP Request数据帧的接收,解析,ARP Reply数据帧的发送,ARP表的实现。新增了DIY TCP/IP初始化模块的参数解析的实现,修改了网络设备结构体的定义,以及网络设备模块通过PF_PACKET发送数据帧的实现。经验证,DIY TCP/IP可以正确接收ARP Request数据帧,并能够在ARP Request请求的目标IP地址与DIY TCP/IP的虚拟IP地址相等时,构建并回复正确的ARP Reply数据帧。在同一局域网中发送ARP Request的主机上的ARP表中,能够查找到DIY TCP/IP的虚拟IP和运行DIY TCP/IP的主机的网络接口硬件地址的映射。本章的ARP模块的覆盖了DIY TCP/IP目前实现的网络设备模块,pdbuf模块,debug模块,utility模块的代码实现,正确回复ARP Reply数据帧,表明这些模块的代码实现是正确的。
当前目录结构
DIY TCP/IP End of CP4
下一篇:DIY TCP/IP IP模块和ICMP模块的实现0

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值