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