AF_PACKET 链路层通信
目前大多数操作系统都为应用程序提供了访问数据链路层的手段,应用程序访问链路层可以监视链路层上收到的分组,这使得我们可以在普通计算机系统上通过像 tcpdump 这样的程序来监视网络,而无需使用特殊的硬件设备,如果使用网络接口的混杂模式,我们甚至可以侦听本地电缆上的所有分组,而不只是以程序运行所在主机为目的地址的分组。
SylixOS 下读取数据链路层分组需要创建 PACKET 类型的套接字,PACKET 套接字用于在链路层上收发数据帧,这样应用程序可以在用户空间完成链路层之上各层的实现,PACKET 套接字的定义方式与 TCP、UDP、UNIX 定义方式类似:
int sockfd;
sockfd = socket(AF_PACKET, type, protocol);
PACKET 套接字的定义需要指定 socket 函数参数 domain 为 AF_PACKET(PF_PACKET),参数 type 支持 SOCK_DGRAM 和 SOCK_RAW 两种,参数 protocol 包含链路层的协议,如下表所示是部分常用的协议(更多的协议在文件 <net/if_ether.h> 中定义)。
指定协议 ETH_P_XXX 通知数据链路层将它收到的那些不同类型的帧传递给 PACKET 套接字,如果数据链路支持混杂模式(如以太网),那么需要设置网络设备的混杂模式。首先通过调用 ioctl 函数(命令 SIOCGIFFLAGS)获得标志,并设置 IFF_PROMISC 标志,然后再次调用 ioctl 函数(命令 SIOCSIFFLAGS)设置新的标志(包含 IFF_PROMISC 标志)。
我们上面提到参数 type 支持 SOCK_RAW 类型,这种类型包含了链路层头部信息的原始分组,也就是说这种类型的套接字在发送的时候需要自己加上一个 ethhdr 结构类型的 MAC 头部,该结构定义如下:
struct ethhdr {
u_char h_dest[ETH_ALEN]; /* destination eth addr */
u_char h_source[ETH_ALEN]; /* source ether addr */
u_short h_proto; /* packet type ID field */
} __attribute__((packed));
下面是 ethhdr 结构成员含义:
- h_dest:以太网的目的 MAC 地址。
- h_source:以太网的源 MAC 地址。
- h_proto:帧类型,如下表所示。
SOCK_DGRAM 类型已经对链路层头部信息进行了处理,即收到的数据帧已经去掉了以太网头部,应用程序发送此类数据时也无需添加头部信息。
创建好的套接字,可以通过调用 recvfrom 函数和 sendto 函数进行数据的接收和发送。与 UDP 不同的是,PACKET 的地址结构是 sockaddr_ll 类型的结构,该结构定义如下:
协议 | 说明 | 值 |
---|---|---|
ETH_P_IP | IP 类型数据帧 | 0x0800 |
ETH_P_ARP | ARP 类型数据帧 | 0x0806 |
ETH_P_RARP | RARP 类型数据帧 | 0x8035 |
ETH_P_ALL | 所有类型的数据帧 | 0x0003 |
struct sockaddr_ll {
u_char sll_len; /* Total length of sockaddr */
u_char sll_family; /* AF_PACKET */
u_short sll_protocol; /* Physical layer protocol */
int sll_ifindex; /* Interface number */
u_short sll_hatype; /* ARP hardware type */
u_char sll_pkttype; /* packet type */
u_char sll_halen; /* Length of address */
u_char sll_addr[8]; /* Physical layer address */
};
- sll_len:地址结构长度。
- sll_family:协议族(AF_PACKET)。
- sll_protocol:链路层协议类型,如上表所示。
- sll_ifindex:网络接口索引号(可以通过 ifconfig 命令查看)。
- sll_hatype:设备协议类型(SylixOS 仅支持 ARPHRD_ETHER)。
- sll_pkttype:分组类型,如下表所示。
- sll_halen:物理地址长度(MAC 地址长度)。
- sll_addr:物理地址。
下表列出了 SylixOS 支持的分组类型,需要注意的是,这些类型只对接收到的分组有意义。
分组类型 | 说明 |
---|---|
PACKET_HOST | 目标地址是本地主机的分组 |
PACKET_BROADCAST | 物理层的广播分组 |
PACKET_MULTICAST | 一个分组发送到物理层的多播地址 |
PACKET_OTHERHOST | 在混杂模式发向其他主机的分组 |
PACKET_OUTGOING | 回环分组 |
AF_PACKET 实例
前面介绍过 AF_PACKET 协议族支持 SOCK_DGRAM 和 SOCK_RAW 类型,前者让内核处理添加或者去除以太网报文头部,而后者则让应用程序对以太网头部具有完全的控制,在 socket 调用过程中协议类型必须符合头文件 <net/if_ether.h> 中定义的类型之一(例如:本例中的 ETH_P_IP),一般使用 ETH_P_IP 来处理 IP 的一组协议(TCP、UDP、ICMP 等)。
本实例程序提供了 AF_PACKET 读取链路层原始数据(SOCK_RAW)的方法,功能上类似于嗅探(Sniffer)技术。程序首先调用 socket 函数建立套接字(协议域为 AF_PACKET,协议类型为 SOCK_RAW),因为要处理 IP 层的数据报,因此指定 protocol 为 ETH_P_IP。随后程序调用 recvfrom 函数开始接收网络包,当成功接收到网络包后,首先判断是否是一个完整的包(长度不能小于 42),如果是一个完整的包则打印其 MAC 地址和网络地址(IP 地址),否则丢弃该包并且程序退出。
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if_ether.h>
#define IPV4_VERSION (0x4)
#define BUFSISE (2048)
int main (int argc, char *argv[])
{
int sockfd;
struct ethhdr *ethheader;
unsigned char *ip4header;
sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP));
if (sockfd < 0) {
perror("socket");
return (-1);
}
while (1) {
ssize_t len;
unsigned char buf[BUFSISE];
fprintf(stdout, "................\n");
len = recvfrom(sockfd, (void *)buf, sizeof(buf), 0, NULL, NULL);
if (len < 0) {
continue;
}
fprintf(stdout, "recv %ld bytes.\n", len);
/*
* 检查数据包是否包含了完整的以太网头(14), IP头(20), TCP/UDP头(20/8)
*/
if (len < 42) {
perror("recvfrom: ");
fprintf(stderr, "Incomplete packet[%s]\n", strerror(errno));
break;
}
/*
* 打印以太网网头信息.
*/
ethheader = (struct ethhdr *)buf;
fprintf(stdout, "Ethernet type: 0x%x\n", ntohs(ethheader->h_proto));
fprintf(stdout, "Source MAC addr: "
"%02x:%02x:%02x:%02x:%02x:%02x\n",
ethheader->h_source[0], (ethheader->h_source[1]),
(ethheader->h_source[2]), ethheader->h_source[3],
(ethheader->h_source[4]), (ethheader->h_source[5]));
fprintf(stdout, "Destination MAC addr: "
"%02x:%02x:%02x:%02x:%02x:%02x\n",
ethheader->h_dest[0], ethheader->h_dest[1],
ethheader->h_dest[2], ethheader->h_dest[3],
ethheader->h_dest[4], ethheader->h_dest[5]);
/*
* 打印IP头信息.
*/
ip4header = buf + sizeof(struct ethhdr);
if (((*ip4header) & IPV4_VERSION) == IPV4_VERSION) {
fprintf(stdout, "Source host %d.%d.%d.%d\n",
ip4header[12], ip4header[13],
ip4header[14], ip4header[15]);
fprintf(stdout, "Destination host %d.%d.%d.%d\n",
ip4header[16], ip4header[17],
ip4header[18], ip4header[19]);
fprintf(stdout, "Source, Dest ports %d,%d\n",
(ip4header[20] << 8) + ip4header[21],
(ip4header[22] << 8) + ip4header[23]);
fprintf(stdout, "Layer[4] protocol %d\n", ip4header[9]);
}
}
close(sockfd);
return (0);
}
在 SylixOS Shell 下运行程序,部分结果显示如下:
# ./AF_PACKET
................
recv 78 bytes.
Ethernet type: 0x800
Source MAC addr: 00:ff:ff:6f:a7:a0
Destination MAC addr: 08:08:3e:26:0a:5a
Source host 192.168.7.40
Destination host 192.168.7.30
Source, Dest ports 2048,1386
Layer[4] protocol 1
................
……
从程序运行结果可以看出:以太网类型为 0x800 正是 IP 协议类型;可以看出发送方和接收方的 MAC 地址以及网络地址;protocol 的值,为 1 表示可以得出该网络包为 ICMP(1)数据包。
从实验的更多结果来看,该程序只接收了目的地址为 192.168.7.30 的数据包,也就是只接收了发向本主机的网络包,实际上,可以设置套接字选项为混杂模式(可以接收非发向本机的数据包)来接收更多的数据包。
AF_PACKET 与 MMAP
上面介绍的 PACKET 套接字传输方式是通过缓冲区的形式,并且每捕获一个分组就需要一个函数调用,这样就造成了传输效率的下降,例如,如果想要获得 PACKET 的时间戳就需要调用两次函数(如 libpcap)。
PACKET MMAP 机制解决了这种传输效率低的问题,PACKET MMAP 机制会在内核空间分配一块内核缓冲区,然后用户通过调用 mmap 函数将此缓冲区映射到用户空间,内核将接收到的分组拷贝到内核缓冲区中,应用程序就可以直接访问缓冲区中的数据。
PACKET MMAP 机制提供了一个映射到用户空间大小可配的环形缓冲区,缓冲区的大小通过 tpacket_req 结构中的成员值来获取,此结构体的定义如下:
struct tpacket_req {
u_int tp_block_size; /* Min size of contiguous block */
u_int tp_block_nr; /* Number of blocks */
u_int tp_frame_size; /* Size of frame */
u_int tp_frame_nr; /* Total number of frames */
};
下面是 tpacket_req 结构成员含义:
- tp_block_size:块大小。
- tp_block_nr:块数。
- tp_frame_size:帧大小。
- tp_frame_nr:帧数。
这个环形缓冲区由 tp_block_nr 个块组成,每一个块中包含了 tp_block_size/tp_frame_size 个 frame,其中每个 frame 必须在同一个块中。块的大小必须是 SylixOS 页对齐的(getpagesize 函数获得的值,SylixOS 默认为 4K),frame 的大小必须是 TPACKET_ALIGNMENT == 16(在 <netpacket/packet.h> 中定义)个字节对齐。需要注意的是,tp_frame_nr 必须与(tp_block_size /tp_frame_size)* tp_block_nr 相同。在 SylixOS 中所有的块组成了一段连续的物理内存。如下图所示显示了这种结构关系。
环形缓冲区的每一个 frame 头部都包含了一个 tpacket_hdr 结构,这个结构存储了一些信息,这种结构分为两种版本的实现,如下所示:
enum tpacket_versions {
TPACKET_V1,
TPACKET_V2
};
下面所示是 TPACKET_V1 版本的实现,含义如下:
struct tpacket_hdr {
volatile u_long tp_status;
volatile u_int tp_len;
volatile u_int tp_snaplen;
volatile u_short tp_mac;
volatile u_short tp_net;
volatile u_int tp_sec;
volatile u_int tp_usec;
};
下面是 tpacket_hdr 结构成员含义:
- tp_status:frame 的状态,如下表所示:。
- tp_len:分组的长度(如果是 SOCK_DGRAM 内核会减去 MAC 头的长度)。
- tp_snaplen:有效数据长度。
- tp_mac:以太网帧偏移位置。
- tp_net:NET 数据报偏移位置。
- tp_sec:时间戳(秒)。
- tp_usec:时间戳(微妙)。
frame 状态 | 说明 |
---|---|
TP_STATUS_KERNEL | 表示内核可以使用该帧,也就是说应用程序没有数据可读 |
TP_STATUS_USER | 表示应用程序可读,此时内核不可以使用该帧 |
下面所示是 TPACKET_V2 版本的实现,含义如下:
struct tpacket2_hdr {
volatile u_int32_t tp_status;
volatile u_int32_t tp_len;
volatile u_int32_t tp_snaplen;
volatile u_int16_t tp_mac;
volatile u_int16_t tp_net;
volatile u_int32_t tp_sec;
volatile u_int32_t tp_nsec;
volatile u_int16_t tp_vlan_tci;
volatile u_int16_t tp_vlan_tpid;
};
- tp_status:frame 的状态。
- tp_len:分组的长度(如果是 SOCK_DGRAM 内核会减去 MAC 头的长度)。
- tp_snaplen:有效数据长度。
- tp_mac:以太网帧偏移位置。
- tp_net:NET 数据报偏移位置。
- tp_sec:时间戳(秒)。
- tp_nsec:时间戳(纳秒)。
- tp_vlan_tci:vlan 中两个字节的标签控制信息(TCI)。
- tp_vlan_tpid:vlan 中两个字节的标签协议标识(TPID)。
下图显示了 SylixOS 实现的 block 和 frame 之间的对应关系,以及 frame 结构。
为了能够正确的使用 mmap 的方式进行 PACKET 通信,需要下面的过程:
- 创建 socket,如下所示:。
int sockfd;
sockfd = socket(AF_PACKET, type, htons(ETH_P_ALL));
- 设置 socket 选项,以建立内核环形缓冲区, req 参数的结构类型为 tpacket_req,如下所示:。
setsocketopt(sockfd, SOL_PACKET, PACKET_RX_RING, (void *)&req, sizeof(req));
- 应用程序映射和使用缓冲区,如下所示:。
mmap(0, size, PROT_READ|PROT_WRITE, MAP_SHARED, sockfd, 0);
通过上面的过程,即可创建一个基于 MMAP 机制的 PACKET 通信,应该程序可以通过调用 poll 函数等待缓冲区数据可读,当这段缓冲区不再需要的时候,只需调用 close 函数关闭创建的 socket 即可。
下面程序实例展示了 MMAP 机制的使用方法:
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <net/if_ether.h>
#include <netpacket/packet.h>
#include <net/if_arp.h>
#include <sys/mman.h>
#include <poll.h>
#define BUF_SIZE (16 * 1024 * 1024) /* 16M */
#define FREAM_SIZE (2 * 1024)
#define BLOCK_SIZE getpagesize() /* 1 页 */
void show_packet (void *arg)
{
struct tpacket_hdr *thdr = (struct tpacket_hdr *)arg;
fprintf(stdout, "=================packet header=================\n");
fprintf(stdout, "tpacket len: %d\n", thdr->tp_len);
fprintf(stdout, "tpacket status: %lu\n", thdr->tp_status);
fprintf(stdout, "tpacket snaplen: %d\n", thdr->tp_snaplen);
fprintf(stdout, "tpacket mac offset: %d\n", thdr->tp_mac);
fprintf(stdout, "tpacket net offset: %d\n", thdr->tp_net);
}
int process_packet (struct tpacket_hdr *thdr, int *idx)
{
/*
* 如果状态为 TP_STATUS_KERNEL 代表没有数据了
*/
if (thdr->tp_status == TP_STATUS_KERNEL) {
return (-1);
}
show_packet((void *)thdr);
thdr->tp_len = 0;
thdr->tp_status = TP_STATUS_KERNEL;
(*idx)++;
(*idx) %= BUF_SIZE / FREAM_SIZE;
return (0);
}
int main (int argc, char *argv[])
{
void *buff = NULL;
int sockfd;
struct tpacket_req req;
int ret;
int tpacket_verion = TPACKET_V1;
int index = 0, i;
struct pollfd pfd;
sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP));
if (sockfd < 0) {
perror("socket");
goto exit2;
}
/*
* 设置TPACKET版本为1
*/
ret = setsockopt(sockfd, SOL_PACKET, PACKET_VERSION,
(void *)&tpacket_verion, sizeof(int));
if (ret < 0) {
perror("setsockopt");
goto exit2;
}
/*
* 设置缓冲区属性
*/
req.tp_block_size = BLOCK_SIZE;
req.tp_frame_size = FREAM_SIZE;
req.tp_block_nr = BUF_SIZE / req.tp_block_size;
req.tp_frame_nr = BUF_SIZE / req.tp_frame_size;
ret = setsockopt(sockfd, SOL_PACKET, PACKET_RX_RING,
(void *)&req, sizeof(req));
if (ret < 0) {
perror("setsocket");
goto exit2;
}
buff = mmap(0, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, sockfd, 0);
if (MAP_FAILED == buff) {
perror("mmap");
goto exit2;
}
for (i = 0; i < req.tp_frame_nr; i++) {
struct tpacket_hdr *thdr;
/*
* 如果在 poll 之前已经发现数据
*/
thdr = (struct tpacket_hdr *)(buff + index * FREAM_SIZE);
if (thdr->tp_status == TP_STATUS_USER) {
goto proc_pkt;
}
pfd.fd = sockfd;
pfd.events = POLLIN;
pfd.revents = 0;
ret = poll(&pfd, 1, -1);
if (ret < 0) {
perror("poll");
goto exit1;
}
proc_pkt:
while (1) {
thdr = (struct tpacket_hdr *)(buff + index * FREAM_SIZE);
if (thdr->tp_status == TP_STATUS_KERNEL) {
break;
}
show_packet((void *)thdr);
thdr->tp_len = 0;
thdr->tp_status = TP_STATUS_KERNEL;
index++;
index %= req.tp_frame_nr;
}
}
exit1:
munmap(buff, BUF_SIZE);
exit2:
close(sockfd);
return (0);
}
在 SylixOS Shell 下运行程序,部分结果显示如下:
# ./AF_PACKET_MMAP
......
=================packet header=================
tpacket len: 73
tpacket status: 1
tpacket snaplen: 73
tpacket mac offset: 82
tpacket net offset: 96
=================packet header=================
......
从上面的实例可以看出,使用 PACKET MMAP 机制接收网络数据无需调用 recv 函数、recvfrom 函数等。