1. 程式人生 > >Linux原始套接字之ARP協議實現

Linux原始套接字之ARP協議實現

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幀示意圖:



乙太網的頭部結構:
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協議示意圖:



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協議示意圖:


http://images.51cto.com/files/uploadimg/20091211/095250676.jpg

可以看出,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幀,然後一層一層的剝去首部,就可以得到終端使用者傳送的資料。