ARP协议在网络编程中有着重要的地位,是一个工作在数据链路层的协议,本文简单介绍了ARP协议,用一个实例查看收到的ARP请求,并对该请求发出ARP回应,实例有完整的源代码,使用C语言在Linux下实现,代码中有详细的注释;阅读本文需要有一定的网络编程基础,了解OSI的网络模型,本文对初学者有一定的难度。

1. ARP协议

  • ARP(Address Resolution Protocol),地址解析协议;在局域网上通过IP地址获取物理地址MAC的协议,该协议工作在数据链路层;

  • 为什么需要ARP协议

    • 以TCP/IP协议为例,应用程序在应用层发出信息后,在传输层(TCP层)加上一个TCP报头,TCP报头中需要填写源端口和目的端口,端口号标识着一台机器上的某个确定的应用程序,在网络层(IP层)加上一个IP报头,IP报头需要填写源IP地址和目的IP地址,IP地址标识着互联网上一台唯一的机器,所以,TCP报头和IP报头可以确定下来互联网上的某台机器上的某个应用程序;
    • 在数据链路层需要给数据包加上以太网报头,在以太网报头中,需要填的是机器的物理地址(MAC地址),源地址和目的地址均要填MAC地址;
    • 在局域网中传输数据,是要依靠数据链层中的MAC地址的,我们可以使用ioctl获取本机的MAC地址,但是却无法获取目的地址的MAC地址,这时就需要使用ARP协议了;
  • ARP的工作原理

    • 在以太网络中,每台机器均有一个ARP cache,里面存着一些IP地址和MAC地址的对应关系,在ubuntu下,内核把ARP cache映射为文件:/proc/net/arp,所以使用 cat /proc/net/arp 是可以看到本机的 ARP cache 的
    • 在数据链路层需要填MAC地址时,首先查询ARP cache中是否有相应的记录,如果有则直接使用;
    • 如果在ARP cache中没有记录,则向局域网广播一个ARP请求,里面有需要获取MAC地址的机器所对应的IP地址,当这个IP地址收到这个ARP请求时,则会回应一个ARP回应,将自己的MAC地址告知发送请求的机器,发出请求的机器收到该回应后,应该在ARP cache中添加上这条记录;
    • 有关ARP cache的操作,请参考文章《如何用C语言操作arp cache》
  • ARP请求/回应的格式

    • 对一个ARP请求的回应被称为ARP回应,ARP请求数据包和回应数据包的结构是一样的;

    arp request


  • 以太网报头(Ethernet Header)

    • 数据链路层的以太网报头定义在头文件<linux/if_ether.h>中:
      1
      2
      3
      4
      5
      
      struct ethhdr {
          unsigned char  h_dest[ETH_ALEN];    /* destination eth addr  */
          unsigned char  h_source[ETH_ALEN];  /* source ether addr  */
          __be16         h_proto;             /* packet type ID field  */
      } __attribute__((packed));
      
    • 上面的ETH_ALAN的值为6,定义在头文件linux/if_ether.h中;
    • h_dest字段为目的MAC地址,h_source字段为源MAC地址;
    • h_proto表示当前数据包在网络层使用的协议,Linux支持的协议在头文件linux/if_ether.h中定义;通常在网络层使用的IP协议,这个字段的值是0x0800(宏ETH_P_IP),对于ARP报文,这个字段的值为0x0806(ETH_P_ARP),表示网络层使用的是ARP协议;
    • 对于一个ARP请求数据包来说,h_dest通常为6个0xff,这是因为发送者并不知道目的机器的MAC地址,发送者的意图就是通过一个ARP请求包获得目的机器的MAC地址,所以发送者需要把这个数据包广播出去,使每台机器都可以收到,6个0xff表示一个广播地址;
    • 对于ARP请求的数据包来说,h_source是发送者自己的MAC地址,发送者在发送ARP请求时,自己的MAC地址是显然已知的,如果不知道,可以通过ioctl()获取(请参考文章《如何使用raw socket发送UDP报文》);
    • 对于ARP回应的数据包来说,目的地址就是发送ARP请求包的机器,h_dest应该就是收到的ARP请求包中的h_source;
    • 对于ARP回应的数据包来说,h_source就是自己的MAC地址,这个当然是已知的;
    • 不管是ARP请求还是回应数据包,h_proto都是0x0806(宏ETH_P_ARP),表示网络层使用的协议是ARP协议
  • ARP报头(ARP header)

    ARP header


    • arp报头定义在头文件linux/if_arp.h中:
      1
      2
      3
      4
      5
      6
      7
      
      struct arphdr {
          __be16          ar_hrd;       /* format of hardware address   */
          __be16          ar_pro;       /* format of protocol address   */
          unsigned char   ar_hln;       /* length of hardware address   */
          unsigned char   ar_pln;       /* length of protocol address   */
          __be16          ar_op;        /* ARP opcode (command)         */
      }
      
    • ar_hrd字段为ARP协议硬件类型,相关定义在<linux/if_arp.h>中,对以太网而言,硬件类型为1(宏ARPHRD_ETHER);
    • ar_pro字段为协议类型,协议类型定义在linux/if_ether.h中,IP协议的协议类型为0x800(宏ETH_P_IP);
    • ar_hln字段为硬件地址的长度,对以太网而言,硬件地址就是MAC地址,长度是6 bytes(宏ETH_ALEN);
    • ar_pln字段为协议地址长度,对IP协议的IPv4而言,协议地址就是IP地址,长度是4 bytes;
    • ar_op字段表示要执行的操作,1-ARP请求,2-ARP回应;
    • 在发送ARP回应时,ARP报头比较好处理,只需要把ar_op从1(宏ARROP_REQUEST)改成2(ARROP_REPLY)即可,其它字段均不需要改动。
  • ARP Payload

    ARP payload


    • 对于以太网IPv4而言,ARP payload的定义如下
      1
      2
      3
      4
      5
      6
      
      struct eth_arpmsg {
          unsigned char   arp_sha[ETH_ALEN];   /* Sender Hardware Address(MAC) */
          unsigned char   arp_spa[4];          /* Sender Protocol Address(IP) */
          unsigned char   arp_dha[ETH_ALEN];   /* Destination Hardware Address(MAC) */
          unsigned char   arp_dpa[4];          /* Destination Protocol Address(IP) */
      }
      
    • ARP payload的实际长度要根据ARP报头来确定,对于以太网IPv4而言,其结构是可以确定的,但对于其它网络而言,硬件地址的表达形式可能不是MAC地址,那么sender hardware address的长度也有可能不是6;协议地址的长度也是一样,比如IPv6的协议地址长度就是128位,16个字节;所以,如果不是以太网,则arp payload是要重新定义的,好在我们日常见到的网络都是以太网IPv4;
    • 对于ARP请求数据包而言,arp_sha和arp_spa为发送者的MAC地址和IP地址,发送者是已知的,接收者的IP也是知道的,所以arp_dpa字段也是填好的,但接收者的MAC地址,也就是arp_dha字段发送者并不知道(发送者发送ARP请求就是为了获得这个MAC),所以收到的ARP请求包里arp_dha字段为6个0;
    • 在回应ARP请求包时,ARP请求包的发送者成为了接收者,arp_dha和arp_dpa(目的机器的MAC和IP)应该就是请求包中的arp_sha和arp_spa;
    • 在ARP回应数据包中,arp_sha和arp_spa就是本机的MAC地址和IP地址,这些信息都是已知的。

2. ARP请求/回应的发送和接收

  • 对于习惯于使用socket基于TCP/IP协议的编程来说,arp的请求既无法使用TCP socket(SOCK_STREAM)发送,也无法使用UDP socket(SOCK_DGRAM)发送;接收也是一样,TCP/UDP socket是无法收到ARP的回应信息的;
  • 前面说过,ARP协议是工作在数据链路层的,而TCP/UDP socket是用在应用层的,应用层发送的数据在传输层要被加上一个TCP/UDP头,在网络层要被加上一个IP头,在数据链路层要被加上一个以太网头,然后才交给物理层(网卡的驱动程序)发送;
  • 相关概念请参考《Linux下如何在数据链路层接收原始数据包》《如何使用raw socket发送UDP报文》
  • 发送ARP请求、接收ARP回应只能在数据链路层完成,要在数据链路层上编程,需要用到raw socket,相关的知识也请参考上面两篇文章;
  • 在发送ARP请求时,封装过程与文章《如何使用raw socket发送UDP报文》中类似;
  • 在接收ARP回应时,拆包过程与文章《Linux下如何在数据链路层接收原始数据包》中类似;
  • 使用程序获取、添加和删除ARP cache记录的方法,请参考文章《如何用C语言操作arp cache》
  • 关于如何构建以太网头、ARP报头和ARP payload,在上一节介绍以太网报头和ARP报文时已经做了介绍。

3. 接收ARP请求并发送回应的实例

  • 在这个例子中,程序接收局域网上所有机器发出的arp请求包,如果这个请求包是发给本机的,即arp请求包中的目的IP为本机IP,则显示收到的以太网报头和arp报文,并向发送者发送一个arp回应;

  • 程序的运行步骤如下:

    1. 创建一个raw socket
    1
    
    int sock_raw = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    
    1. 查询网络接口的索引号(interface index)
    1
    2
    3
    4
    5
    6
    7
    8
    
    #define DEVICE    "enp0s3"
    struct ifreq ifr;
    
    memset(&ifr, 0, sizeof(struct ifreq));
    strncpy(ifr.ifr_name, DEVICE, IFNAMSIZ - 1);
    ioctl(sock_raw, SIOCGIFINDEX, &ifr);
    
    int ifindex = ifr.ifr_ifindex;
    
    1. 查询本机的MAC地址
    1
    2
    3
    
    unsigned char src_mac[ETH_ALEN];
    ioctl(sock_raw, SIOCGIFHWADDR, &ifr);
    memcpy(src_mac, ifr.ifr_hwaddr.sa_data, ETH_ALEN);
    
    1. 查询本机的IP地址
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    #define DEVICE    "enp0s3"
    struct ifreq ifr;
    
    memset(&ifr, 0, sizeof(struct ifreq));
    strncpy(ifr.ifr_name, DEVICE, IFNAMSIZ - 1);
    if (ioctl(sock_raw, SIOCGIFADDR, &ifr) < 0) {
        printf("SIOCGIFADDR.\n");
        close(sock_raw);
        exit(EXIT_FAILURE);
    }
    uint32_t src_ip;
    memcpy((void *)&src_ip, &(((struct sockaddr_in *)&(ifr.ifr_addr))->sin_addr), sizeof(uint32_t));
    
    1. 为发送arp报文设置sockaddr_ll
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    struct sockaddr_ll saddr_ll;
    
    memset((void *)&saddr_ll, 0, sizeof(struct sockaddr_ll));
    saddr_ll.sll_family   = PF_PACKET;
    saddr_ll.sll_protocol = htons(ETH_P_IP);
    saddr_ll.sll_ifindex  = ifindex;
    saddr_ll.sll_hatype   = ARPHRD_ETHER;
    saddr_ll.sll_pkttype  = PACKET_OTHERHOST;
    saddr_ll.sll_halen    = ETH_ALEN;
    
    1. 等待接收来自网络的数据包
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    struct __attribute__((packed)) arp_packet {
        struct arphdr arp_hdr;
        unsigned char arp_sha[ETH_ALEN];
        unsigned char arp_spa[4];
        unsigned char arp_dha[ETH_ALEN];
        unsigned char arp_dpa[4];
    };
    int buf_size = sizeof(struct ethhdr) + sizeof(struct arp_packet);
    void *buffer = (void *)malloc(buf_size);
    
    memset(buffer, 0, buf_size);
    recvfrom(sock_raw, buffer, buf_size, 0, NULL, NULL);
    
    1. 如果数据包不是arp数据包,则忽略,回到步骤6
    2. 如果数据包不是arp请求包,则忽略,回到步骤6
    3. 如果arp请求包不是发给本机的,则忽略,回到步骤6
    4. 显示以太网报头
    5. 显示arp数据包(arp header + arp payload)
    6. 构建用于发送arp reply的以太网报头
    1
    2
    3
    4
    5
    6
    
    unsigned char *eth_header = buffer;
    
    memcpy((void *)eth_header, (const void *)(eth_header + ETH_ALEN), ETH_ALEN);
    memcpy((void *)(eth_header + ETH_ALEN), (const void *)src_mac, ETH_ALEN);
    struct ethhdr *eh = (struct ethhdr *)buffer;
    eh->h_proto = ETH_P_ARP;
    
    1. 构建用于发送arp reply的arp数据包
    1
    2
    3
    4
    5
    6
    7
    8
    
    unsigned char *arp_header = buffer + sizeof(struct ethhdr);
    struct arp_packet *ah = (struct arp_packet *)arp_header;
    ah->arp_hdr.ar_op = ARPOP_REPLY;
    
    memcpy(ah->arp_dha, ah->arp_sha, ETH_ALEN);
    memcpy(ah->arp_dpa, ah->arp_spa, 4);
    memcpy(ah->arp_sha, src_mac, ETH_ALEN);
    memcpy(ah->arp_spa, (unsigned char *)&src_ip, 4);
    
    1. 将sockaddr_ll中的sll_addr字段填为目的MAC地址
    1
    
    memcpy(saddr_ll.sll_addr, eh->h_dest, ETH_ALEN);
    
    1. 显示用于发送arp reply的以太网报头
    2. 显示用于发送arp reply的arp数据包内容
    3. 发送arp reply
    1
    
    sendto(sock_raw, buffer, buf_size, 0, (struct sockaddr *)&saddr_ll, sizeof(struct sockaddr_ll));
    
    1. 回到步骤6
  • 该程序的主体在一个死循环中,所以程序只能使用ctrl+c退出,为了保证退出时能很好地清理现场,程序中拦截了SIGINT信号,当按下ctrl+c时,程序将收到这个信号,进行现场清理并退出;

  • 还是要简单讨论一下构建发送arp reply的arp数据包的过程:

    1. 当收到一个arp请求时,其中的arp头部分基本不需要变,唯一要变的ar_op字段,因为这个字段在收到的数据包里为1,意即这是arp request,这个字段要改为2,意即这是一个arp reply
    2. 收到的arp报文中arp_sha为发送人的MAC,发送arp reply时,这个MAC应该成为收件人MAC,即:arp_dha,所以要将arp_sha拷贝到arp_dha
    3. 收到的arp报文中arp_spa为发送人的IP,发送arp reply时,这个IP应该成为收件人IP,即:arp_dpa,所以要将arp_spa拷贝到arp_dpa
    4. 发送arp reply时,发件人MAC,也就是arp_sha应该为本机的MAC
    5. 发送arp reply时,发件人IP,也就是arp_spa应该为本机IP
  • 构建以太网报头的过程:

    1. 收到的以太网报头中的收件人的MAC(h_dest),应该为6个0xff,因为发送者发送arp request时并不知道接收者的MAC,所以发的是广播报文;
    2. 收到的以太网报头中的发件人的MAC(h_source),在发送arp reply时应该为以太网报头中的收件人的MAC(h_dest);
    3. 发送arp reply时的发件人的MAC应该填本机的MAC;
  • 由于ARP工作在数据链路层,所以不能使用普通的socket进行接收和发送,需要使用raw socket,使用raw socket时需要使用一个结构(struct sockaddr_ll),这个结构的作用类似于IPv4的socket编程中的(struct sockaddr_in)的作用,有关这个结构的简单介绍,可以参考文章《如何使用raw socket发送UDP报文》

  • 由于这个程序是使用raw socket在数据链路层上接收数据,所以实际上程序可以收到在局域网上的所有数据包,程序过滤掉了不是ARP协议的数据包,然后又过滤掉了目的IP不是本机的数据包,在ctrl+c退出时,程序会显示收到了多少数据包,其中ARP协议数据包有多少,ARP请求包有多少,程序一共回应了多少个数据包,数据还是有点意思的;

  • 下面是源程序,文件名:arp-request-and-reply.c(点击文件名下载源程序)

  • 编译,程序在ubuntu 20.04下编译通过,gcc版本为9.4.0

    1
    
    gcc -Wall arp-request-and-reply.c -o arp-request-and-reply
    
  • 运行,由于使用了raw socket,必须要要有root权限才可以运行

    1
    
    sudo ./arp-request-and-reply
    
  • 如果看不到有发给本机的arp请求,可以将你的手机连接到你得电脑所在的局域网上,然后,在你的电脑上开一个新的终端,在新的终端上ping一下你手机的IP,通常会激活手机向你的机器发送一个arp请求,当然你也可以ping另一台局域网中的电脑试试;

  • 运行截图

    screenshot of running arp_server

欢迎订阅 『网络编程专栏』


欢迎访问我的博客:https://whowin.cn

email: hengch@163.com

donation