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帧,然后一层一层的剥去首部,就可以得到最终用户发送的数据。
====
http://blog.csdn.net/chenjin_zhong/article/details/7272156