《UNIX网络编程 卷1》 笔记补充内容: 发送任意以太网帧

    xiaoxiao2021-03-25  193

    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/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 *) (packet + len); 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 *) (packet + len); udph->source = htons(1234); udph->dest = htons(5678); udph->check = 0; /* UDP校验和,填充0即可 */ len += sizeof(struct udphdr); /* 添加数据 */ packet[len++] = 'a'; packet[len++] = 'b'; packet[len++] = 'c'; packet[len++] = 'd'; /* 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)); /* 指定出接口索引 */ bzero(&dstaddr, sizeof(dstaddr)); dstaddr.sll_ifindex = if_index.ifr_ifindex; dstaddr.sll_halen = ETH_ALEN; /* 发送以太网帧 */ if (sendto(sockfd, packet, len, 0, (struct sockaddr*)&dstaddr, sizeof(dstaddr)) < 0) printf("send failed\n"); return 0; }

    使用SOCK_RAW套接字,数据包会原封不动地发送给由目的地址(代码中的dstaddr)指定的接口,所以用户可以任意构造数据包。

    用tcpdump 开启-e选项在出接口eth2抓包结果如下:

    说明: 以上测试环境是 CentOS 6.5 虚拟机。

    我们构造的以太网帧是以RFC894定义的格式封装的,这也是最常见的以太网封装格式。但还有一种是RFC1042定义的封装格式,也叫做IEEE802.3封装格式,这两种封装格式是不同的。

    那么如何构造IEEE802.3封装格式的以太网帧?一种简单的方法就是使用SOCK_DGRAM类型的套接字,它使得我们不用自己构造以太网首部,但是需要自己填充802.2 LLC首部。下面给出一个使用SOCK_DGRAM套接字的例子:

    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, "enp2s0"); if (ioctl(sockfd, SIOCGIFINDEX, &if_index) < 0) { perror("SIOCGIFINDEX"); exit(1); } /* 获取出接口IP地址 */ bzero(&if_ip, sizeof(if_ip)); strcpy(if_ip.ifr_name, "enp2s0"); 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] = 0x03; packet[6] = 0x08; /* IP数据报 0x0800 */ packet[7] = 0x00; len += 8; /* 构造IP首部 */ struct iphdr *iph = (struct iphdr *) (packet + len); 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 *) (packet + len); udph->source = htons(1234); udph->dest = htons(5678); udph->check = 0; len += sizeof(struct udphdr); /* 添加数据 */ packet[len++] = 'a'; packet[len++] = 'b'; packet[len++] = 'c'; packet[len++] = 'd'; /* UDP数据报的长度 */ udph->len = htons(len - 8 - sizeof(struct iphdr)); /* IP数据报的长度 */ iph->tot_len = htons(len - 8); /* 计算IP首部校验和 */ iph->check = in_cksum((unsigned short *)iph, sizeof(struct iphdr)); /* 指定出接口索引 */ bzero(&dstaddr, sizeof(dstaddr)); dstaddr.sll_family = AF_PACKET; dstaddr.sll_protocol = ETH_P_PUP; dstaddr.sll_ifindex = if_index.ifr_ifindex; dstaddr.sll_halen = ETH_ALEN; /* 填充目的MAC地址 */ for(i = 0; i < 6; i++) dstaddr.sll_addr[i] = 0xff; /* 发送以太网帧 */ if (sendto(sockfd, packet, len, 0, (struct sockaddr*)&dstaddr, sizeof(dstaddr)) < 0) printf("send failed!\n"); return 0; }

    使用tcpdump开启-e选项抓包结果如下:

    说明: 以上测试环境是 Ubuntu 16.04 虚拟机。

    转载请注明原文地址: https://ju.6miu.com/read-388.html

    最新回复(0)