红联Linux门户
Linux帮助

Linux网络编程:使用AF_PACKET域套接字发送任意以太网帧

发布时间:2017-04-23 10:09:58来源:linux网站作者:为爱走天涯2000
Linux提供了packet套接字,使得用户层可以从设备驱动层(链路层)接收以太网帧或者发送以太网帧到设备驱动层。
packet_socket = socket(AF_PACKET, int socket_type, int protocol);
socket_type参数为SOCK_RAW或SOCK_DGRAM。两者的区别是SOCK_RAW要求用户自己构造以太网首部,而SOCK_DGRAM则由内核来构造以太网首部。
protocol参数是IEEE 802.3定义的协议字段,以网络字节序存储。定义在<linux/if_ether.h>中。常见的协议类型是IP协议(ETH_P_IP)和ARP协议(ETH_P_ARP)。
sockaddr_ll结构体用来表示与设备无关的链路层地址。
 
struct sockaddr_ll {
unsigned short sll_family; /* Always AF_PACKET */
unsigned short sll_protocol; /* Physical-layer protocol */
int sll_ifindex; /* Interface number */
unsigned short sll_hatype; /* ARP hardware type */
unsigned char sll_pkttype; /* Packet type */
unsigned char sll_halen; /* Length of address */
unsigned char sll_addr[8]; /* Physical-layer address */
};
 
sll_protocol和上面的socket调用protocol参数一致。
sll_ifindex为接口索引号。
sll_hatype为ARP硬件类型。对于以太网就是ARPHRD_ETHER。
sll_pkttype为数据包类型,接收时用来过滤数据包。
sll_addr和sll_halen分别为链路层地址和长度。
 
下面给出使用AF_PACKET域SOCK_RAW套接字发送包含UDP数据报的以太网帧的例子。
 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <linux/if_packet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/udp.h>
#include <net/if.h>
#include <net/ethernet.h>
#include <arpa/inet.h>
/*有些头文件可能不需要*/
/*校验和计算函数,取自《UNIX网络编程卷1》*/
uint16_t in_cksum(uint16_t *addr, int len)
{
int nleft = len;
uint32_t sum = 0;
uint16_t *w = addr;
uint16_t answer = 0;
while (nleft > 1)  {
sum += *w++;
nleft -= 2;
}
if (nleft == 1) {
*(unsigned char *)(&answer) = *(unsigned char *)w ;
sum += answer;
}
sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >> 16);
answer = ~sum;
return(answer);
}
int main(int argc, char **argv)
{
int sockfd;
int i, len = 0;
char packet[1024];
struct ifreq if_index, if_mac, if_ip;
struct sockaddr_ll dstaddr;
/*创建AF_PACKET域SOCK_RAW套接字*/
if ((sockfd = socket(AF_PACKET, SOCK_RAW, 0)) == -1) {
perror("socket");
exit(1);
}
/*获取出接口索引*/
bzero(&if_index, sizeof(if_index));
strcpy(if_index.ifr_name, "eth2");
if (ioctl(sockfd, SIOCGIFINDEX, &if_index) < 0) {
perror("SIOCGIFINDEX");
exit(1);
}
/*获取出接口MAC地址*/
bzero(&if_mac, sizeof(if_mac));
strcpy(if_mac.ifr_name, "eth2");
if (ioctl(sockfd, SIOCGIFHWADDR, &if_mac) < 0) {
perror("SIOCGIFHWADDR");
exit(1);
}
/*获取出接口IP地址*/
bzero(&if_ip, sizeof(if_ip));
strcpy(if_ip.ifr_name, "eth2");
if (ioctl(sockfd, SIOCGIFADDR, &if_ip) < 0)
perror("SIOCGIFADDR");
memset(packet, 0, sizeof(packet));
/*构造以太网首部*/
struct ether_header *eth = (struct ether_header *)packet;
for (i = 0; i < 6; i++) /*源MAC地址*/
eth->ether_shost[i] = ((struct sockaddr_ll *)&if_mac.ifr_hwaddr)->sll_addr[i];
for (i = 0; i < 6; i++) /*目的MAC地址*/
eth->ether_dhost[i] = 0xff;
eth->ether_type = htons(ETH_P_IP); /*类型为IP数据报*/
len += sizeof(struct ether_header);
/*构造IP首部*/
struct iphdr *iph = (struct iphdr *) (eth + 1);
iph->ihl = 5; /*以4字节为单位的IP首部长度*/
iph->version = 4; /*IP协议版本号*/
iph->tos = 16; /*服务类型TOS*/
iph->id = htons(54321); /*标识*/
iph->ttl = 64; /*TTL*/
iph->protocol = 17; /*UDP协议*/
iph->saddr = ((struct sockaddr_in *)&if_ip.ifr_addr)->sin_addr.s_addr; /*源IP地址*/
iph->daddr = inet_addr("192.168.2.100"); /*目的IP地址*/
len += sizeof(struct iphdr);
/*构造UDP首部*/
struct udphdr *udph = (struct udphdr *) (iph + 1);
udph->source = htons(1234);
udph->dest = htons(5678);
udph->check = 0; /*UDP校验和,填充0即可*/
len += sizeof(struct udphdr);
/*添加数据*/
packet[len++] = 0x1;
packet[len++] = 0x2;
packet[len++] = 0x3;
packet[len++] = 0x4;
/*UDP数据报的长度*/
udph->len = htons(len - sizeof(struct ether_header) - sizeof(struct iphdr));
/*IP数据报的长度*/
iph->tot_len = htons(len - sizeof(struct ether_header));
/*计算IP首部校验和*/
iph->check = in_cksum((unsigned short *)iph, sizeof(struct iphdr) / 2);
/*指定出接口索引*/
bzero(&dstaddr, sizeof(dstaddr));
dstaddr.sll_ifindex = if_index.ifr_ifindex;
dstaddr.sll_halen = ETH_ALEN;
/*发送以太网帧*/
while (1) {
if (sendto(sockfd, packet, len, 0, (struct sockaddr*)&dstaddr, sizeof(dstaddr)) < 0)
printf("Send failed\n");
sleep(1);
}
exit(0);
}
 
使用SOCK_RAW套接字,数据包会原封不动地发送给由目的地址(代码中的dstaddr)指定的接口,所以用户可以任意构造数据包。
用tcpdump 开启-e选项在出接口eth2抓包结果如下:
Linux网络编程:使用AF_PACKET域套接字发送任意以太网帧
 
我们构造的以太网帧是以RFC894定义的格式封装的,这也是最常见的以太网封装格式。但还有一种是RFC1042定义的封装格式,也叫做IEEE802.3封装格式,这两种封装格式是不同的。
Linux网络编程:使用AF_PACKET域套接字发送任意以太网帧
 
那么如何构造IEEE802.3封装格式的以太网帧?一种简单的方法就是使用SOCK_DGRAM类型的套接字,它使得我们不用自己构造以太网首部,但是需要自己填充802.2 LLC首部。下面给出一个使用SOCK_DGRAM套接字的例子的main函数。
 
int main(int argc, char **argv)  
{  
int sockfd;  
int i, len = 0;  
char packet[1024];  
struct ifreq if_index, if_ip;  
struct sockaddr_ll dstaddr;  
/*创建AF_PACKET域SOCK_DGRAM套接字*/  
if ((sockfd = socket(AF_PACKET, SOCK_DGRAM, 0)) == -1) {  
perror("socket");  
exit(1);  
}  
/*获取出接口索引*/  
bzero(&if_index, sizeof(if_index));  
strcpy(if_index.ifr_name, "eth2");  
if (ioctl(sockfd, SIOCGIFINDEX, &if_index) < 0) {  
perror("SIOCGIFINDEX");  
exit(1);  
}  
/*获取出接口IP地址*/  
bzero(&if_ip, sizeof(if_ip));  
strcpy(if_ip.ifr_name, "eth2");  
if (ioctl(sockfd, SIOCGIFADDR, &if_ip) < 0)  
perror("SIOCGIFADDR");  
memset(packet, 0, sizeof(packet));  
/*填充802.2 LLC首部*/  
packet[0] = 0xaa;  
packet[1] = 0xaa;  
packet[2] = 3;  
packet[6] = 0x08; /*IP数据报*/  
packet[7] = 0x00;  
/*构造IP首部*/  
struct iphdr *iph = (struct iphdr *) (packet + 8);  
iph->ihl = 5; /*以4字节为单位的IP首部长度*/  
iph->version = 4; /*IP协议版本号*/  
iph->tos = 16; /*服务类型TOS*/  
iph->id = htons(54321); /*标识*/  
iph->ttl = 64; /*TTL*/  
iph->protocol = 17; /*UDP协议*/  
iph->saddr = ((struct sockaddr_in *)&if_ip.ifr_addr)->sin_addr.s_addr; /*源IP地址*/  
iph->daddr = inet_addr("192.168.2.254"); /*目的IP地址*/  
len += sizeof(struct iphdr);  
/*构造UDP首部*/  
struct udphdr *udph = (struct udphdr *) (iph + 1);  
udph->source = htons(1234);  
udph->dest = htons(5678);  
udph->check = 0;
len += sizeof(struct udphdr);  
/*添加数据*/  
packet[len++] = 0x1;  
packet[len++] = 0x2;  
packet[len++] = 0x3;  
packet[len++] = 0x4;  
/*UDP数据报的长度*/  
udph->len = htons(len - sizeof(struct iphdr));  
/*IP数据报的长度*/  
iph->tot_len = htons(len);  
/*计算IP首部校验和*/  
iph->check = in_cksum((unsigned short *)iph, sizeof(struct iphdr) / 2);  
/*指定出接口索引*/  
bzero(&dstaddr, sizeof(dstaddr));  
dstaddr.sll_ifindex = if_index.ifr_ifindex;  
dstaddr.sll_halen = ETH_ALEN;  
/*填充目的MAC地址*/  
for(i = 0; i < 6; i++)  
dstaddr.sll_addr[i] = 0xff;    
/*发送以太网帧*/  
while (1) {  
if (sendto(sockfd, packet, len + 8, 0, (struct sockaddr*)&dstaddr, sizeof(dstaddr)) < 0)  
printf("Send failed\n");  
sleep(1);  
}  
return 0;  
}
 
使用tcpdump开启-e选项抓包结果如下:
Linux网络编程:使用AF_PACKET域套接字发送任意以太网帧
 
本文永久更新地址:http://www.linuxdiyf.com/linux/30236.html