Linux原始套接字之ARP协议实现

时间:2022-10-29 11:00:18

1. ARP协议介绍

 ARP(AddressResolutionProtocol)地址解析协议用于将计算机的网络地址(IP地址32位)转化为物理地址(MAC地址48位)[RFC826].ARP协议是属于链路层的协议,在以太网中的数据帧从一个主机到达网内的另一台主机是根据48位的以太网地址(硬件地址)来确定接口的,而不是根据32位的IP地址。内核(如驱动)必须知道目的端的硬件地址才能发送数据。当然,点对点的连接是不需要ARP协议的。ARP工作时,首先请求主机会发送出一个含有所希望到达的IP地址的以太网广播数据包,然后目标IP的所有者会以一个含有IP和MAC地址对的数据包应答请求主机。这样请求主机就能获得要到达的IP地址对应的MAC地址,同时请求主机会将这个地址对放入自己的ARP表缓存起来,以节约不必要的ARP通信。ARP协议是工作在数据链路层,基于以太网. 所以,必须了解以太网的MAC帧格式和ARP协议格式.


MAC帧示意图:

Linux原始套接字之ARP协议实现

以太网的头部结构:
struct ether_header
{
u_int8_t ether_dhost[ETH_ALEN];      // destination eth addr
u_int8_t ether_shost[ETH_ALEN];      // source ether addr   
u_int16_t ether_type;                 // packet type ID field
} __attribute__ ((__packed__));
整个以太网的头部包括: 目的地址(6字节),源地址(6字节),类型(2字节),帧内数据(46-1500个字节),CRC校验和(4字节)
#define ETH_ALEN 6 //以太网地址的长度
#define ETH_HALEN 14 //以太网头部的总长度  (6+6+2)
#define ETH_ZLEN 60 //不含CRC校验数据的数据最小长度(46+14)
#define ETH_DATA_LEN 1500  //帧内数据的最大长度
#define ETH_FRAME_LEN 1514//不含CRC最大以太网长度(1500+14)

ARP协议示意图:

Linux原始套接字之ARP协议实现

ARP头部信息:
struct arphdr{
 __be16 ar_hrd;//硬件类型 1-硬件接口为以太网接口-2字节
 __be16 ar_pro;//协议类型-0x0800高层协议为IP协议 -2字节
 unsigned char ar_hln;//硬件地址长度-6字节 MAC-1字节
 unsigned char ar_pln;//协议地址长度-4字节为IP-1字节
 __be16 ar_op;//ARP操作码-1 ARP请求-2字节

}
ARP协议数据结构:
struct ether_arp{
    struct arphdr ea_hdr; //ARPfixed-size header(ARP固定大小的报头)-8字节
    u_char arp_sha[ETHER_ADDR_LEN]; //sender hardware address(发送端硬件地址)-6字节
    u_char arp_spa[4]; //sender protocol address(发送端协议地址)-4字节
    u_char arp_tha[ETHER_ADDR_LEN]; // target hardware address(接收端硬件地址)-6字节
    u_char arp_tpa[4]; //target protocol address(接收端协议地址)-4字节
};
#define arp_hrd ea_hdr.ar_hrd
#define arp_pro ea_hdr.ar_pro
#define arp_hln ea_hdr.ar_hln
#define arp_pln ea_hdr.ar_pln
#define arp_op ea_hdr.ar_op

ARP的头部一共是8个字节.ARP数据部分一共是20字节,所以ARP协议的长度是28个字节.


带以太网首部的ARP协议示意图:


Linux原始套接字之ARP协议实现

可以看出,ARP协议长度为28个字节,以太网为14个字节,一共42字节而MAC帧的最小长度60个字节,因此必须增加18个字节的填充,构成ARP包.

以太网首部的帧类型用来指示上层协议的类型,是IP还是ARP.


2. ARP请求实例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <net/if.h>
#include <sys/types.h>
#include <asm/types.h>
#include <features.h> /* 需要里面的 glibc 版本号 */
#if __GLIBC__ >= 2 && __GLIBC_MINOR >= 1
        #include <netpacket/packet.h>
        #include <net/ethernet.h> /* 链路层(L2)协议 */
#else
        #include <asm/types.h>
        #include <linux/if_packet.h>
        #include <linux/if_ether.h> /* 链路层协议 */
#endif
#include <netinet/if_ether.h>
/**
以太网的头部结构:
struct ether_header
{
u_int8_t ether_dhost[ETH_ALEN];      // destination eth addr
u_int8_t ether_shost[ETH_ALEN];      // source ether addr   
u_int16_t ether_type;                 // packet type ID field
} __attribute__ ((__packed__));
整个以太网的头部包括: 目的地址(6字节),源地址(6字节),类型(2字节),帧内数据(46-1500个字节),CRC校验和(4字节)
#define ETH_ALEN 6 //以太网地址的长度
#define ETH_HALEN 14 //以太网头部的总长度  (6+6+2)
#define ETH_ZLEN 60 //不含CRC校验数据的数据最小长度(46+14)
#define ETH_DATA_LEN 1500  //帧内数据的最大长度
#define ETH_FRAME_LEN 1514//不含CRC最大以太网长度(1500+14)
ARP头部信息:
struct arphdr{
 __be16 ar_hrd;//硬件类型 1-硬件接口为以太网接口
 __be16 ar_pro;//协议类型-0x0800高层协议为IP协议 
 unsigned char ar_hln;//硬件地址长度-6字节 MAC
 unsigned char ar_pln;//协议地址长度-4字节为IP
 __be16 ar_op;//ARP操作码-1 ARP请求

}
ARP协议数据结构:
struct ether_arp{
    struct arphdr ea_hdr; //ARPfixed-size header(ARP固定大小的报头)
    u_char arp_sha[ETHER_ADDR_LEN]; //sender hardware address(发送端硬件地址)
    u_char arp_spa[4]; //sender protocol address(发送端协议地址)
    u_char arp_tha[ETHER_ADDR_LEN]; // target hardware address(接收端硬件地址)
    u_char arp_tpa[4]; //target protocol address(接收端协议地址)
};
#define arp_hrd ea_hdr.ar_hrd
#define arp_pro ea_hdr.ar_pro
#define arp_hln ea_hdr.ar_hln
#define arp_pln ea_hdr.ar_pln
#define arp_op ea_hdr.ar_op

sockaddr_ll为设备无关的物理层地址结构,描述发送端的地址结构

struct sockaddr_ll
{
unsigned short sll_family;  总填 AF_PACKET 
unsigned short sll_protocol; 网络序列的物理层协议号 0x806为ARP协议
int sll_ifindex;  接口编号  eth0对应的编号 
unsigned short sll_hatype; 头部类型 ARPHRD_ETHER为以太网
unsigned char sll_pkttype; 包类型 PACKET_HOST
unsigned char sll_halen;  地址长度  MAC地址长度6字节
unsigned char sll_addr[8];物理地址 MAC地址只用了前面的6字节
};

FF:FF:FF:FF:FF:FF
SOCK_RAW原始套接字的分析:
(1)socket(AF_INET,SOCK_RAW,IPPROTO_TCP|IPPROTO_UDP|IPPROTO_ICMP);//发送或接收ip数据包,得到原始的IP包
(2)socket(PF_PACKET,SOCK_RAW,htons(ETH_P_IP|ETH_P_ARP|ETH_P_RAP|ETH_P_ALL));//发送或接收以太网数据帧
(1)使用第一种套接字类型,能得到发往本机的原始的IP数据包,但不能得到发往非本机的IP数据包,被过滤了,也不能得到从本机发出去的数据包。这类协议可自己组织TCP,ICMP,UDP数据包。
(2)第二种套接字能收到发往本地的MAC帧,也能收到从本机发出去的数据帧(第3个参数为ETH_P_ALL),能接收到非发往本地的MAC数据帧(网卡需要设置为promisc混杂模式)
协议类型:
ETH_P_IP 0X800 只接收发往本机的mac的ip类型的数据帧
ETH_P_ARP 0X806 只接收发往本机的arp类型的数据帧
ETH_P_RARP 0x8035 只接受发往本机的rarp类型的数据帧
ETH_P_ALL 0X3    接收发往本机的MAC所有类型ip,arp,rarp数据帧,接收从本机发出去的数据帧,混杂模式打开的情况下,会接收到非发往本地的MAC数据帧
此时设备无关的物理地址使用struct sockaddr_ll
从而得到MAC帧
**/

//发送ARP数据,ARP协议结构+以太网头部
int main(int argc,char*argv[]){
     struct arppacket {
                struct ether_header eh;//以太网的头部
                struct ether_arp ea;//arp包数据结构
                u_char padding[18];//填充位,ARP包的最小长度是60个字节,不包括以太网的帧的CRC校验和
        } arpreq;//不包含CRC校验的以太网的帧的最小长度为60个字节
    int fd;
        if((fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_RARP))) < 0) {//发送ARP数据包
                perror("Socket error");
                exit(1);
        }

        
        bzero(&arpreq, sizeof(arpreq));
        /* 填写以太网头部*/
    //目的MAC
        char eth_dest[ETH_ALEN]={0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
       //源MAC
        char eth_source[ETH_ALEN]={0x00,0x13,0xD4,0x36,0x98,0x34};
        memcpy(arpreq.eh.ether_dhost,eth_dest, ETH_ALEN);
        memcpy(arpreq.eh.ether_shost, eth_source, ETH_ALEN);
        arpreq.eh.ether_type = htons(ETHERTYPE_ARP);//协议类型ARP协议

        /* 填写arp数据 */
        arpreq.ea.arp_hrd = htons(ARPHRD_ETHER);//硬件类型,主机字节序转换成网络字节序
        arpreq.ea.arp_pro = htons(ETHERTYPE_IP);//协议类型
        arpreq.ea.arp_hln = ETH_ALEN;//MAC地址长度6字节
        arpreq.ea.arp_pln = 4;//IP地址长度
        arpreq.ea.arp_op = htons(ARPOP_REQUEST);//操作码,ARP请求包
        memcpy(arpreq.ea.arp_sha, eth_source, ETH_ALEN);
    char *source_ip="222.27.253.108";
        struct in_addr source;
        inet_pton(AF_INET,source_ip,&source);
    /**
    struct in_addr{
        u32 s_addr;
       }
       这个结构体只有一个变量,所以结构体的地址与变量的地址是一样的
       u_char arp_spa[4]是4字节的unsigned char型变量,unsigned char型是数值型,所以一共是32位
       这样就把source.s_addr内存地址处的4字节二进制IP地址复制到内存地址arp_spa处,而source与source.s_addr
       内存地址是一致的,所以可以直接用source的地址    
    **/
        memcpy(arpreq.ea.arp_spa, &source, 4);//源IP
    char *dst_ip="222.27.253.1";
        struct in_addr dst;
        inet_pton(AF_INET,dst_ip,&dst);
    //目的IP
        memcpy(arpreq.ea.arp_tpa,&dst,4); 
    struct sockaddr_ll to;
    bzero(&to,sizeof(to));
    to.sll_family = PF_PACKET;
    to.sll_ifindex = if_nametoindex("eth0");//返回对应接口名的编号
    int i=0;
    for(i=0;i<1;i++){
     int sendsize=sizeof(struct ethhdr)+sizeof(struct ether_arp);
     int size=sendto(fd,&arpreq,sizeof(arpreq),0,(struct sockaddr*)&to,sizeof(to));//发送arp请求包
     printf("size=%d\n",size);

    }

    //接收ARP响应包
    char buffer[ETH_FRAME_LEN];
    bzero(buffer,ETH_FRAME_LEN);
    struct sockaddr_ll to1;
    bzero(&to1,sizeof(to1));
    int len=sizeof(to1);
    int recvfd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ARP));//只接受发往本抽的ARP帧
    for(;;){
      int size=recvfrom(recvfd,buffer,ETH_FRAME_LEN,0,(struct sockaddr*)&to1,&len);
      //从以太网包头开始输出值
      //以太网的目的MAC
      struct ether_header *h1=(struct ether_header*)buffer;
     //ARP包
      struct ether_arp* arp=(struct ether_arp*)(buffer+14);//以太网的源MAC6个字节 ,目的MAC 6个字节,类型为2字节
      printf("目的MAC:");
      for(i=0;i<ETH_ALEN;i++){
    printf("%02x-",h1->ether_dhost[i]);
      }
      printf("\n");
     //源MAC地址
      printf("源MAC:");
      for(i=0;i<ETH_ALEN;i++){
    printf("%02x-",h1->ether_shost[i]);

      }
     //以太网的帧类型
     printf("\n");
     printf("帧类型:%0x\n",ntohs(h1->ether_type));//十六进制ARP包
    //判断是否是ARP响应包,如果是操作方式码为2
    //printf("%d\n",(ntohs)(arp->arp_op));//一定要把网络字节序转换为主机字节序
    if((ntohs)(arp->arp_op)==2){
    //硬件类型
    printf("硬件类型:%0x\n",(ntohs)(arp->arp_hrd));//1代表硬件接口为以太网接口
    //协议类型
    printf("协议类型:%0x\n",(ntohs)(arp->arp_pro));//0x800代表高层协议为IP
    //硬件地址长度
    printf("硬件地址长度:%0x\n",arp->arp_hln);
    //协议地址长度
    printf("协议地址长度:%0x\n",arp->arp_pln);
    //发送方的MAC地址
    printf("发送方的MAC:");
    for(i=0;i<ETH_ALEN;i++){
      printf("%02x-",arp->arp_sha[i]);
    }
    printf("\n");
    printf("发送方的IP:");
    char ip[16];
    inet_ntop(AF_INET,arp->arp_spa,&ip,16);//arp_spa是一个unsigned char数组
    printf("%s\n",ip);
    printf("接收方的硬件地址:");
    for(i=0;i<ETH_ALEN;i++){
      printf("%02x-",arp->arp_tha[i]);
    }
    printf("\n");
    //接收方的IP地址
    bzero(&ip,16);
    printf("接收方的IP地址为:");
    inet_ntop(AF_INET,arp->arp_tpa,&ip,16);
    printf("%s\n",ip);

    break;
    }

    

    }

return 1;
}

运行结果:./arp

size=60
目的MAC:00-13-d4-36-98-34-
源MAC:00-0f-e2-5f-3c-8c-
帧类型:806
硬件类型:1
协议类型:800
硬件地址长度:6
协议地址长度:4
发送方的MAC:00-0f-e2-5f-3c-8c-
发送方的IP:222.27.253.1
接收方的硬件地址:00-13-d4-36-98-34-
接收方的IP地址为:222.27.253.108

总结:本文主要介绍了以太网的帧结构以及ARP协议,并给出了ARP请求与响应的具体实例。PF_PACKET的原始套接字可以得到MAC帧,然后在MAC帧之上构建ARP数据包即可。通过这个例子,我们可以通过监听网卡,得到经过本机的MAC帧,然后一层一层的剥去首部,就可以得到最终用户发送的数据。

====

http://blog.csdn.net/chenjin_zhong/article/details/7272156