在Linux下实现comer的TCP/IP协议栈--ARP地址解析协议(下)

时间:2022-11-24 10:34:32

CHAPTER2:寻路的向导——ARP

5、 arp_in.c ARP输入过程

 

在讲arp_in.c前我们应该先看一下ni_in.c,这是comer中的多路分解的实现,arp_in.c就在其中被调用。在前面的《网络接口层》一文中我们看到,当packet_rx函数从pcap得到数据包后,通过调用ni_in.c将数据包传送给协议栈,ni_in.c根据数据包的不同类型进行多路分解,传送给不同层次的协议。下面先看一下ni_in.c的源码。

 

/* ni_in.c 传入分组的多路分解 (2006.4.5) */

 

//#include <conf.h>

#include <kernel.h>

#include <network.h>

 

//#include <ospf.h>

 

int arp_in(struct netif*,struct ep*);

int rarp_in(struct netif*,struct ep*);

int ip_in(struct netif*,struct ep*);

 

int ni_in(struct netif *pni,struct ep *pep,unsigned len)

{

    int rv;

   

    pep->ep_ifn = pni - &nif[0];//用当前端口的地址减去0端口的地址得到端口号

  

    pni->ni_ioctets += len;//累加收到的总字节

    //判断是否为广播分组,累加相应的计数器

    if (!memcmp(pni->ni_hwa.ha_addr,pep->ep_dst,EP_ALEN))

       pni->ni_iucast++;//广播包

    else

       pni->ni_inucast++;//非广播包

    //多路分解的实现

    switch (net2hs(pep->ep_type))

    {

    case EPT_ARP:  rv = arp_in(pni,pep);break;

    //case EPT_RARP: rv = rarp_in(pni,pep);break;

    //case EPT_IP:   rv = ip_in(pni,pep);break;

    default://不是这三种协议,则抛弃包

         pni->ni_iunkproto++;

         free(pep);//c库中的free释放,分配的时候也用malloc

         rv = OK;

    } 

    return rv;

}

 

comer中多路分解的实现远比你想像的简单,仅仅使根据以太网帧的帧类型字段判断该由什么协议接收它。这比linux中的多路分解简单多了,你不需要在net_proto_family结构中注册协议,也不需要使用dev_add_pack函数注册packet_type结构来指明接收什么样的数据包以及使用什么函数接收。当然,ni_in.c的简单在于这里仅仅支持TCP/IP协议,但作为一个教学的演示,这已经足够了。这里以太网帧只有三种类型:ARPIPRARP。我们的TCPUDPICMP等都封装在IP中,它们的多路分解是由IP层实现的,这在后面讲解IP层时会提到。这里,我们仅仅关心的是ARP包。由于现在还没有实现IP 层以及RARP协议,所以它们的接收函数都被注释掉了。到目前为止,我们的协议栈只接收ARP包。这里需要的是注意的是switch (net2hs(pep->ep_type)) 语句,comer在此没有做网络到本机的字节顺序转换,而是在xinu中的struct ep * ee_get(ped, prfd, prbd) ee_demux.c)函数中实现,但我们把它放在了这里,毕竟,对数据包的任何修改都应该放在协议栈中处理,我们的网络接口层不需要完成太多工作(正如在讲arpsend函数时说的一样,这样更规范一点)。好了,现在非常清楚了,我们的网络接口层调用了ni_in函数,ni_in做出判断后,将相应协议的数据包提交给相应协议的输入函数处理,这里ARP的处理函数就是arp_in

 

/* arp_in.c --arp输入过程(2006.4.5)*/

 

#include <kernel.h>

#include <network.h>

 

void arpqsend(struct arpentry *);

 

int arp_in(struct netif *pni,struct ep *pep)

{

     struct arp *parp = (struct arp *) pep->ep_data;

     struct arpentry  *pae;

     int                     arplen;

          

     //先做网络字节到本机字节的转换

     parp->ar_hwtype = net2hs(parp->ar_hwtype);

     parp->ar_prtype  = net2hs(parp->ar_prtype);

     parp->ar_op        = net2hs(parp->ar_op);

 

     //如果硬件类型不是以太网,协议类型不是IP协议的包丢弃

     //换言之,只处理以太网IP协议的ARP请求

     if (parp->ar_hwtype != pni->ni_hwtype || parp->ar_prtype != EPT_IP)

            {

          free(pep);

               return OK;

            }

 

     //更新ARP缓存表项,并重新设定表项超时时间

     if (pae = arpfind(SPA(parp),parp->ar_prtype,pni))

            {

          memcpy(pae->ae_hwa,SHA(parp),pae->ae_hwlen);

               pae->ae_ttl = ARP_TIMEOUT;     

            }

     //请求若不是本机ip地址,丢弃包

     if (memcmp(TPA(parp),&pni->ni_ip,IP_ALEN))

            {

           free(pep);

             return OK;         

            }

 

      //如果缓存表中没有对应于收到包中的物理地址表项

      //增加之

      if (pae == NULL)

           pae = arpadd(pni,parp);

 

      //检查是否有队列等待发送,有则发送

   

      if (pae->ae_state == AS_PENDING)

           {

          pae->ae_state = AS_RESOLVED;

               arpqsend(pae);

           }

     

       //是否请求本机物理地址,是则应答

       if (parp->ar_op == AR_REQUEST)

       {

         parp->ar_op = AR_REPLY;

             //用收到包的源地址填写目的地址,并在

             //源物理地址中填上本机的物理地址即可

             memcpy(TPA(parp),SPA(parp),parp->ar_prlen);

             memcpy(THA(parp),SHA(parp),parp->ar_hwlen);

             //填写以太网帧头物理地址 

           memcpy(pep->ep_dst,THA(parp),EP_ALEN);

             //填写arp  

             memcpy(SHA(parp),pni->ni_hwa.ha_addr,pni->ni_hwa.ha_len);

             memcpy(SPA(parp)   ,&pni->ni_ip,IP_ALEN);

 

             parp->ar_hwtype = hs2net(parp->ar_hwtype);

             parp->ar_prtype  = hs2net(parp->ar_prtype);

             parp->ar_op        = hs2net(parp->ar_op);

                  

      

             arplen = ARP_HLEN + 2 * (parp->ar_prlen + parp->ar_hwlen);

             link_write((u_char *)pep,EP_HETHLEN + arplen); 

          

       }

       else

              free(pep);

 

          return OK;   

}

 

arp_in.c主要做了两件事,先查找ARP高速缓存表,确定是否绑定了收到包中的ip地址,如没绑定则增加一个绑定,如正在等待绑定,则发送与该表项关联的队列。此外,收到的如果是ARP请求,则发出回应。源码没有大的变动,这里的link_write函数使用方法和arpsend.c中一样。

 

6、 arpalloc.c ARP高速缓存表项分配

 

/* arpalloc.c arp 缓存表项分配(2006.4.8) */

 

/*arp 缓存表中选取一个表项,

  *  存放新的地址绑定

  */

 

//#include <conf.h>

#include <kernel.h>

#include <network.h>

 

void arpdq(struct arpentry *);

 

 

struct arpentry *arpalloc()

{

        static int aenext = 0;

         struct arpentry *pae;

         int i;

         //循环查找,是否有空闲项

         for (i = 0;i < ARP_TSIZE;++i)

                   {

                            if (arptable[aenext].ae_state == AS_FREE)

                               break;

                            aenext = (aenext + 1) % ARP_TSIZE;

                   }

         //如果没有空闲项,将当前aenext 指向的表项分配出去

         pae = & arptable[aenext];

         aenext = (aenext + 1) % ARP_TSIZE;

 

         //如果表项有等待的队列,则将队列释放

         if (pae->ae_state == AS_PENDING && pae->ae_queue >= 0)

                   arpdq(pae);

         pae->ae_state = AS_PENDING;

 

         return pae;

}

 

对这个函数没有任何改动,函数在comer书中已经解释的很清楚。一个有趣的地方是comer提到了使用循环法(round-robin)可能出现的糟糕情况。我曾考虑根据表项的TTL值来分配新表项,但这样的实现过于复杂,影响效率。暂时让它这样吧,等协议栈实现完后我们再来考虑它的改进。

 

7、 arptimer.c ARP高速缓存定期维护管理

 

/* arptimer.c arp 缓存表定时维护(2006.4.8)*/

 

/* arptimer 是被定时进程调用的,将缓存表中超时表项

  *删除

  */

 

//#include <conf.h>

#include <kernel.h>

#include <network.h>

 

#include <pthread.h>

 

void arpdq(struct arpentry *);

int arpsend(struct arpentry *);

 

//参数gran 是上一次进程被调用和此次调用的时间间隔

//删除表项仅仅是标记表项为可用(AS_FREE)

void arptimer(double gran) //gran int 类型,精度是否过小?

{

         struct arpentry *pae;

         int i;

 

         pthread_mutex_lock(&arp_mutex);

 

         for (i = 0;i < ARP_TSIZE;++i)

                   {

                       if ((pae = &arptable[i])->ae_state == AS_FREE)

                                     continue;

                       //ARP_INF表示该表项不受超时限制

                       if (pae->ae_ttl == ARP_INF)

                                   continue;

                      //如果ae_ttl 减去gran 这个时间差小于0,表示超时了

                      //这时,如果表项还在等待地址转换(ae_state = AS_PENDING)

                      //则检查表项的重试次数(ae_attempts) 如果超过最大

                      //重试次数(4 ),删除表项,否则再次重发

                       if ((pae->ae_ttl -= gran) <= 0)

                                {

                                     if (pae->ae_state == AS_RESOLVED)

                                           pae->ae_state = AS_FREE;

                                 else if (++(pae->ae_attempts) > ARP_MAXRETRY)

                                        {

                                               pae->ae_state = AS_FREE;

                                               arpdq(pae);

                                        }

                                  else

                                       {

                                               pae->ae_ttl = ARP_RESEND;

                                               arpsend(pae);

                                       }

 

                                }

                   }

         pthread_mutex_unlock(&arp_mutex);

}

 

这个函数用于定期维护ARP缓存表,改动很小,我把传入的时间差参数granint型改成了double型,这样做的目的是为了提高时间差的精度。Arptimer被协议栈中的一个统一定时器进程slowtimer调用,这在ip层中会看到。Comer提出只要做一个很小的改动就能提高定时器的精度,其实就是将时间差参数的类型用更高精度的类型代替,例如这里的double

同样,函数里的disable(关中断)操作换成了linux下的互斥锁,原因在前面已经讲到。

 

8、 arpdq.c 释放和ARP表项相关的队列

 

//#include <conf.h>

#include <kernel.h>

#include <network.h>

 

#define DUBUG

 

//这里我们用本机做试验,为了能单独编译出ARP 模块

//我们将产生ICMP 报文的代码关掉

void arpdq(struct arpentry *pae)

{

         struct ep *pep;

         struct ip  *pip;

 

         if (pae->ae_queue < 0)

                   return;

        

#ifndef  DUBUG

         while (pep = (struct ep *)deq(pae->ae_queue))

                   {

                            if (gateway && pae->ae_prtype == EPT_IP)

                                     {

                                               pip = (struct ip *) pep->ep_data;

                                               icmp(ICT_DESTUR,ICC_HOSTUR,pip->ip_src,pep,0);

                                     }

                            else

                                     free(pep);

                   }

#endif      

 

         freeq(pae->ae_queue);

         pae->ae_queue = EMPTY;

}

 

程序很简单,需要注意的是这里用一个预处理命令关闭了while循环中的代码。这些代码是在本机作为路由器时向源机器发送一个目的不可达的icmp报文。现在我们还没有实现icmp协议,为了不影响我们的ARP协议工作,暂时把它关掉了。所以,在这里arpdq仅仅做了一件事情,就是释放与ARP表项相关的分组。

 

9、 arpinit ARP初始化

 

/* arpinit.c arp 初始化 */

 

/*这里要初始化和RARP 一起使用的信号量以及几个数据项

  *,为了编译出单独的ARP 模块,我们暂时注释掉相关代码

  */

 

  //#include <conf.h>

  #include <kernel.h>

  //#include <proc.h>

  #include <network.h>

 

  //int rarpsem;

  //int rarppid;

 

  struct arpentry arptable[ARP_TSIZE];

 

  //仅仅初始化ARP 缓存表

  void arpinit()

  {

         int i;

 

         /*

                   删除了部分代码,这里到时用linux 下的方法初始化和rarp 相关数据

         */

         pthread_mutex_init(&arp_mutex,NULL); //初始化ARP 中的互斥锁

         for (i = 0;i < ARP_TSIZE;++i)

                   arptable[i].ae_state = AS_FREE;

                  

  }

 

arpinit主要初始化了ARP高速缓存表。在comer中,arpinit还初始化了一些和RARP相关的变量和信号量,在这里先把它删除,等到实现RARP时我们再用linux下的方法来初始化它们。pthread_mutex_init(&arp_mutex)被放在了这里,它初始化我们在ARP协议中用到的互斥锁。

 

 

以上便是整个ARP协议的实现,它围绕一张ARP高速缓存表进行相关操作,所以arp.h中定义的arpentry结构非常重要,它是整个ARP协议的核心,在读这些程序前,搞懂arpentry结构的每个字段是非常重要的,当然,它也非常简单,花不了大家多少时间。下面,为了测试我们的ARP协议能否正常工作,我设计了一个实例来调用这些函数,它的主要功能就是向用户指定的ip地址发送一个arp请求,不断查询我们的ARP高速缓存表,一旦发现了绑定就打印出来,获得我们请求ip地址的物理地址。

 

三、ARP协议测试实例

在使用这个实例前,有一些注意事项。因为我是在VMware虚拟机下安装的linux进行试验,虚拟机的网络连接方式设置为桥接:直接连接到物理网络 使用其它方式可能会产生收不到ARP响应的问题,我也没有时间去查找问题,但这绝对不是ARP协议的问题,估计是必须将虚拟机中的linux视为网络上的一台真实主机才能正常工作(实际上我还没搞懂其它几种连接方式的原理,所以不能推断出原因。嘿嘿,再次显现出了我的业余)。另外,我也没在真实安装linux的情况下测试的程序(主要没有空间格盘装系统了),但如果你有一个真实的ip地址,并连接到网络中了,程序都应该能正常运行。最后,运行程序前你应该已经安装了libpcaplibnet库,不然我们的网络接口层肯定无法工作。

 

实例运行效果:

 

在Linux下实现comer的TCP/IP协议栈--ARP地址解析协议(下)

源码:

/*arp.c - arp测试实例(2006.4.15)*/

 

#include <kernel.h>

#include <znetwork.h>

#include <sys/types.h>

#include <arpa/inet.h>

#include <sys/mman.h>

#include <fcntl.h>

#include <unistd.h>

 

 

extern int arpsend(struct arpentry * );

extern struct arpentry *arpalloc();

extern void arpinit();

IPaddr trans_ip(u_char *);

void print_hwadd(u_char *);

void print_ipadd(u_char * );

 

//接收mmap 系统调用返回的指针

struct arpentry *paetbl;

 

int main(int argc,char *argv[])

{

 

       u_char * str_ip;

       IPaddr ip;

         struct arpentry *pae;

         int i;

 

         if (argc <= 1)

           {

                   printf("Usage:arptest <IP>/n");

                   exit(0);

            }

 

         printf("===This is a example for testing our ARP protocal stack===/n");

 

         //将用户输入的字符串转化为ip 地址

         str_ip = argv[1];

         if ((ip = trans_ip(str_ip)) == SYSERR)

           {

              printf("error ip!/n");

                   printf("For example:./arptest 218.194.42.156/n");

                   exit(0);

            }

         //调用其它函数前,必须先调用ini_interface() 初始化网络接口

       if (init_interface() == SYSERR)

         {

                   exit(0);

         }

      else

            {

             printf("---The info of interface---/n");

             printf("interface_ip :");

             print_ipadd((u_char *) &nif[1].ni_ip);

             printf("interface_net_ip :");

             print_ipadd((u_char *) &nif[1].ni_net);

             printf("interface_mask :");

             print_ipadd((u_char *) &nif[1].ni_mask);

             printf("MAC : ");

             print_hwadd((u_char *) &nif[1].ni_hwa.ha_addr);

             printf("Subnet brcIP:");

             print_ipadd((u_char *) &nif[1].ni_subnet);

             printf("nbrc:");

             print_ipadd((u_char *) &nif[1].ni_nbrc);

 

            }

         //分配一个ARP高速缓存表项,填入用户输入的ip地址

         //并发送arp 请求

      pae = (struct arpentry*)arpalloc();

          pae->ae_pni = &nif[1];

          pae->ae_hwtype = AR_HARDWARE;

          pae->ae_prtype = EPT_IP;

          pae->ae_prlen = IP_ALEN;

          pae->ae_hwlen = EP_ALEN;

          pae->ae_queue = EMPTY;

          memcpy(pae->ae_pra,&ip,pae->ae_prlen);

          pae->ae_attempts = 0;

          pae->ae_ttl = 0;

          arpsend(pae);

          

         //内存映射 ,供网络接口层的packer_rx 函数调用

         //arptbl_add函数将arp 缓存表写入共享内存

       paetbl = (struct arpentry *)mmap(NULL,sizeof(struct arpentry) * ARP_TSIZE,

               PROT_WRITE|PROT_READ,MAP_SHARED|MAP_ANONYMOUS,-1,0);

         //开启一个字进程接收数据包

         if (fork() == 0)

            {

        

                   if (pcap_receive() == SYSERR)

                      {

                          munmap(paetbl,sizeof(struct arpentry) * ARP_TSIZE);

                            exit(0);     

                       }

          }

         else

           { 

           //主进程不断查询共享内存中的ARP 缓存表

           //一旦发现了绑定则打印出物理地址

                printf("/n");

                   while(1)

                      {

                           

                            printf("LOADING .../n");

                            sleep(1);

                            for (i =0;i < ARP_TSIZE;++i)

                                     {

                                               if ((paetbl + i)->ae_state == AS_RESOLVED)

                                                        {

                                                                 printf("-- The arpentry info --/n");

                                                                 printf("arpentry number:%d/n",i);

                                                                 printf("state:%d/n",(paetbl + i)->ae_state);

                                                               printf("Pro addr:");

                                                                 print_ipadd((paetbl + i)->ae_pra);

                                                                 printf("MAC:");

                                                                 print_hwadd((paetbl + i)->ae_hwa);

                                                                 exit(0);

                                                        }

                                                       

                                     }       

                           

                      }

               

          }

          

}

 

//转化用户输入的字符串为ip 地址

IPaddr trans_ip(u_char * ip_addr)

{

       if (inet_addr(ip_addr) == INADDR_NONE)

              return SYSERR;

           

         return (IPaddr)inet_addr(ip_addr);

}

//打印ip地址

void print_ipadd(u_char *ipadd)

{

  int i;

  for (i = 0;i <3;i++)

    printf("%d.",ipadd[i]);

  printf("%d/n",ipadd[i]);

 

}

//打印硬件地址

void print_hwadd(u_char *hwadd)

{

     int i;

     for (i = 0;i < 5;i++)

          printf("%2x:",hwadd[i]);

     printf("%2x/n",hwadd[i]);     

}

//packet_rx 函数中被调用,像共享内存中写入ARP缓存表

void arptbl_add(struct arpentry *tbl_prt)

{

       int i;

           

         if (tbl_prt == NULL)

                   return;

        

         for(i = 0;i < ARP_TSIZE;++i)

                   memcpy((tbl_prt + i),&arptable[i],sizeof(struct arpentry));

}

 

整个程序比较简单,首先调用init_interface()初始化网络接口层,接着调用arpinit初始化了ARP高速缓存。然后填写一个arpentry结构的高速缓存表项,发送arp请求。紧接着,调用了fork函数开启一个子进程,在其中调用pcap_receive函数接收数据包。并在父进程中不断查询共享内存中的ARP高速缓存表,一旦找到了已绑定的地址就把它打印出来,然后退出程序。

 

在开启新进程前,使用了paetbl = (struct arpentry *)mmap(NULL,sizeof(struct arpentry) * ARP_TSIZE,PROT_WRITE|PROT_READ,MAP_SHARED|MAP_ANONYMOUS,-1,0) 进行了内存映射。为什么要这样做呢?原因很简单,在linux下创建的子进程将复制父进程的内存区域,这样,我们的子进程(用于接收数据包)和父进程(用于打印缓存表)就各自拥有了一个ARP缓存表,子进程向自己的缓存表中写数据,父进程却在查找自己的缓存表,当然无法得到返回的物理地址。所以,这里使用mmap匿名映射,开辟出一块共享内存,子进程将接收到了ARP响应填入共享内存中的缓存表中,父进程则不断的查询共享内存以得到期望的结果。子进程用arptbl_add向共享内存中写入数据,该函数被网络接口层的packet_rx函数调用(还记得《网络接口层》一文中我们所提到的和接口层无关的那个函数吗?就是它了:))。之所以必须把arptbl_add放在那里,是因为用libpcappcap_loop函数捕获数据包是一个不会返回的无限循环,在它之后的代码都不会被执行到,故必须把arptbl_add放在pcap_loop所指定的回调函数中(也就是packet_rx)。嘿嘿,也许高手们已经发现这个程序的两个小bug了。其一,在父进程中,打印的是第一被查找到的已绑定的ARP表项。如果执行程序的同时我们的主机正好收到了ARP数据包,那么很可能打印出的物理地址并不是我们请求的ip地址所对应的(当然,打印的时候把对应的ip地址也打了出来,对比一下就知道了)。这个问题很好解决,在判断语句中加一个用ip地址做判断的条件即可。其二,在找到符合的表项并打印后,父进程就退出了。这时还有一个收尾工作没做——没有给我们的子进程发出结束信号。你可以在程序结束后用ps命令看看,是不是还有一个名为arp的进程在工作,必须用kill命令手动删除。呵呵,我太懒了,发现这两个问题的时候已经不想改了,反正不影响操作。

 

 

好了,其它的代码就非常简单了,无非做了一些打印的工作,就不多说了。如果你对linux下的多进程编程非常熟悉,那么这些代码对你来说应该是非常简单的。不太熟悉的朋友可以查阅一下相关的书籍,很快就能上手。

 

最后,再说一个libnetlibnet_link_write函数的一个奇怪bug。在编译后,链接成可执行程序时,包含该函数的zinterface.c文件必须在调用该函数的文件(也就是我们的arp.c)前

被链接,程序才能正常执行,不然会在sa.sll_ifindex = get_iface_index(l->fd, l->device)libnetlibnet_write_link的源文件)处出错。我还第一次遇到程序的执行和链接顺序有关,这个问题我可是整整找了一天才发现啊,现在也不知道原因,不知道有没有高手出来讲解一下:)

 

ARP讲完了,下面就该进行ip层的编写了,hoho,开始工作!

 

                                                                                           ――未完待续