一 前言
好久没写文章了,这个写文章的事情还是要勤快呀,不然极其容易懈怠。给自己找的理由是,一忙,二是没有好的题目去写,也没有遇到值得分享的主题,这次发现自己对流量捕获的一些内容还有理解模糊的地方,有些只是知道,却缺乏实践、所以有了这篇。
二 流量捕获简介
所谓流量捕获,对网络中的网络流量进行捕获,然后进行分析,用于网络监控、协议审计、威胁检测等多个领域。流量捕获作为数据源头,注重的是是否足够灵活,比如进行各种抓包条件过滤,只抓取自己感兴趣的流量,是否能捕获超大流量,一般超过10Gbps以上的,而不丢包。要做到这一点难度还是挺大的,本文不是阐述高深的高性能流量捕获的相关主题,而是最基本,最简单的流量捕获技术,这是很多高深的流量捕获技术的基础。
三 AF_PACKET抓包方式
3.1 AF_PACKET 基础抓包操作
AF_PACKET socket 准许用户空间的应用在数据链路层捕获数据包,所以它可以查看数据链路层以上的传输层、应用层的所有内容。
说明:实验环境为Centos8.4环境下。
AF_PACKET的socket创建如下:
int fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
- 参数AF_PACKET 定义了socket的类型。
- SOCK_RAW是捕获带14字节的链路层头的数据包,如果不带则传入SOCK_DGRAM.
- 第三个参数是代表协议,来自
if_ether.h
为网络字节序,查看如下:cat /usr/include/linux/if_ether.h
ETH_P_ALL代表所有协议的都捕获,如果只抓IP协议的流量,通过ETH_P_IP来指定。
如果协议设置为0,则不能捕获任何数据包。
所有进入网卡的数据包,会在传递给内核协议栈之前传给socket。创建后socket之后那,就和一般的socket类似,可以进行绑定,这里面主要是绑定到具体的网卡(也可以不绑定,不绑定就是从所有的网卡去收数据),不像tcp socket绑定到特定的ip和端口上,然后就可以通过recvmsg进行收包了。
迫不及待的尝试下最简单的抓包:
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <linux/ip.h>
//#include <linux/in.h>
#include <linux/if_ether.h>
#include <arpa/inet.h>
#include <netinet/udp.h>
#define BUF_LEN 2048
void print_mac(unsigned char *mac)
{
printf("mac: %02x:%02x:%02x:%02x:%02x:%02x", mac[6], mac[7], mac[8], mac[9], mac[10], mac[11]);
printf("-> ");
printf("%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
printf("\n");
}
void print_ip(unsigned char *ip)
{
struct iphdr *iph = (struct iphdr *)ip;
if (iph->version == 4) {
printf("ip: %d.%d.%d.%d ",
iph->saddr & 0xFF, (iph->saddr >> 8) & 0xFF,
(iph->saddr >> 16) & 0xFF, (iph->saddr >> 24) & 0xFF);
printf("-> ");
printf("%d.%d.%d.%d\n",
iph->daddr & 0xFF, (iph->daddr >> 8) & 0xFF,
(iph->daddr >> 16) & 0xFF, (iph->daddr >> 24) & 0xFF);
}
printf("\n");
}
int main(int argc, char *argv[])
{
char buf[BUF_LEN];
unsigned char * ipheader,* etheader;
int sockfd = socket(AF_PACKET,SOCK_RAW,htons(ETH_P_ALL));
if (sockfd < 0) {
perror("socket");
return -1;
}
while (1) {
int ret = recvfrom(sockfd, buf, BUF_LEN, 0, NULL, NULL);
if (ret < 0) {
perror("recvfrom");
return -1;
}
printf("recv %d bytes\n", ret);
if (ret < sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr)) {
continue;
}
etheader = buf;
printf("--------------one packet--------------\n");
print_mac(etheader);
ipheader = buf + sizeof(struct ethhdr);
if ( ((struct ethhdr*)etheader)->h_proto == htons(ETH_P_IP)) {
print_ip(ipheader);
if ( ((struct iphdr*)ipheader)->protocol == IPPROTO_UDP) {
printf("proto:UDP\n");
} else if ( ((struct iphdr*)ipheader)->protocol == IPPROTO_TCP) {
printf("proto:TCP\n");
}
}
printf("-------------------------------------\n\n");
}
close(sockfd);
return 0;
}
结果显示:
recv 54 bytes
--------------one packet--------------
mac: 00:1c:42:38:02:32-> 00:1c:42:00:00:18
ip: 10.211.55.10 -> 18.245.60.61
proto:TCP
-------------------------------------
recv 1514 bytes
--------------one packet--------------
mac: 00:1c:42:00:00:18-> 00:1c:42:38:02:32
ip: 18.245.60.61 -> 10.211.55.10
proto:TCP
-------------------------------------
...
以上代码虽然能工作,但是有几个缺点:
- 抓所有的网卡数据,没有限制网卡;
- 只能抓本机的,不能抓局域网的,所以需要修改下。
利用bind函数可以限定抓哪个网卡:
bind函数使用如下:
struct sockaddr_ll addr;
memset(&addr, 0, sizeof(addr));
addr.sll_family = AF_PACKET;
addr.sll_protocol = htons(ETH_P_ALL);
addr.sll_pkttype = PACKET_HOST;
addr.sll_ifindex = if_nametoindex(gInterfaceName);
bind(fd, (struct sockaddr *)&addr, sizeof(addr));
关于struct 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_family 固定为AF_PACKET; 2. sll_protocol 为协议,同socket,不然绑定会失败;
- sll_ifindex 网卡对应的序列号; 4. sll_pkttype 为包类型,取值:PACKET_HOST为本地包、
PACKET_MULTICAST 为广播包、PACKET_OTHERHOST是在网卡在混杂模式下收到的发送其他主机的包、PACKET_OUTGOING;- sll_halen 和sll_addr 为mac地址的长度和具体的mac地址;
结合起来得到第二个例子:
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <linux/ip.h>
//#include <linux/in.h>
#include <linux/if_ether.h>
#include <arpa/inet.h>
#include <netinet/udp.h>
#include <sys/ioctl.h>
#include <string.h>
#include <net/if.h>
#include <linux/if_packet.h>
#define BUF_LEN 2048
void print_mac(unsigned char *mac)
{
printf("mac: %02x:%02x:%02x:%02x:%02x:%02x", mac[6], mac[7], mac[8], mac[9], mac[10], mac[11]);
printf("-> ");
printf("%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
printf("\n");
}
void print_ip(unsigned char *ip)
{
struct iphdr *iph = (struct iphdr *)ip;
if (iph->version == 4) {
printf("ip: %d.%d.%d.%d ",
iph->saddr & 0xFF, (iph->saddr >> 8) & 0xFF,
(iph->saddr >> 16) & 0xFF, (iph->saddr >> 24) & 0xFF);
printf("-> ");
printf("%d.%d.%d.%d\n",
iph->daddr & 0xFF, (iph->daddr >> 8) & 0xFF,
(iph->daddr >> 16) & 0xFF, (iph->daddr >> 24) & 0xFF);
}
printf("\n");
}
int main(int argc, char *argv[])
{
char buf[BUF_LEN];
unsigned char * ipheader,* etheader;
struct packet_mreq sock_params;
int sockfd = socket(AF_PACKET,SOCK_RAW,htons(ETH_P_ALL));
if (sockfd < 0) {
perror("socket");
return -1;
}
// 获取网卡号
struct ifreq req;
char *eth_name = "em2";
strncpy(req.ifr_name, eth_name, strlen(eth_name)+1);
int ret=ioctl(sockfd, SIOCGIFINDEX, &req);
if (ret < 0) {
perror("ioctl");
return -1;
}
/* if (ioctl(sockfd,SIOCGIFFLAGS,&req)==-1) {
perror("ioctl");
return -1;
}
req.ifr_flags |= IFF_PROMISC;
if (ioctl(sockfd,SIOCSIFFLAGS,&req)==-1) {
perror("ioctl promisc");
return -1;
}
*/
struct sockaddr_ll addr;
addr.sll_family = AF_PACKET;
addr.sll_ifindex = req.ifr_ifindex;
addr.sll_protocol = htons(ETH_P_ALL);
// 采用这种方式设置的混杂模式才能绑定成功
memset(&sock_params, 0, sizeof(sock_params));
sock_params.mr_type = PACKET_MR_PROMISC;
sock_params.mr_ifindex = addr.sll_ifindex;
ret = setsockopt(sockfd, SOL_PACKET, PACKET_ADD_MEMBERSHIP,(void *)&sock_params, sizeof(sock_params));
if (ret < 0) {
perror("setsockopt");
return -1;
}
ret = bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
if (ret < 0) {
perror("bind");
return -1;
}
while (1) {
ret = recvfrom(sockfd, buf, BUF_LEN, 0, NULL, NULL);
if (ret < 0) {
perror("recvfrom");
return -1;
}
printf("recv %d bytes\n", ret);
if (ret < sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr)) {
continue;
}
etheader = buf;
printf("--------------one packet--------------\n");
print_mac(etheader);
ipheader = buf + sizeof(struct ethhdr);
if ( ((struct ethhdr*)etheader)->h_proto == htons(ETH_P_IP)) {
print_ip(ipheader);
if ( ((struct iphdr*)ipheader)->protocol == IPPROTO_UDP) {
printf("proto:UDP\n");
} else if ( ((struct iphdr*)ipheader)->protocol == IPPROTO_TCP) {
printf("proto:TCP\n");
}
}
printf("-------------------------------------\n\n");
}
close(sockfd);
return 0;
}
ok ,现在我们这个代码已经可以指定网卡,和支持设置混杂模式了,这时候又遇到问题了,收到的包太多了,在高速网络情况下,会有大量丢包,所以我们需要进行捕获的数据包进行过滤,只收我们需要的包,这个通过bpf设置过滤表达式,bpf过滤表达式,如何转成c代码可以利用的代码,可以通过tcpdump来转换,tcpdump -d bpf过滤表达式
通过这个命令得到的类似汇编的bpf语言,通过-dd
选项来生成可以用c语言使用的表达式。
tcpdump命令举例:
[root@opengauss-master socket_test]# tcpdump -d host 10.21.3.124
(000) ldh [12]
(001) jeq #0x800 jt 2 jf 6
(002) ld [26]
(003) jeq #0xa15037c jt 12 jf 4
(004) ld [30]
(005) jeq #0xa15037c jt 12 jf 13
(006) jeq #0x806 jt 8 jf 7
(007) jeq #0x8035 jt 8 jf 13
(008) ld [28]
(009) jeq #0xa15037c jt 12 jf 10
(010) ld [38]
(011) jeq #0xa15037c jt 12 jf 13
(012) ret #262144
(013) ret #0
[root@opengauss-master socket_test]# tcpdump -dd host 10.21.3.124
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 4, 0x00000800 },
{ 0x20, 0, 0, 0x0000001a },
{ 0x15, 8, 0, 0x0a15037c },
{ 0x20, 0, 0, 0x0000001e },
{ 0x15, 6, 7, 0x0a15037c },
{ 0x15, 1, 0, 0x00000806 },
{ 0x15, 0, 5, 0x00008035 },
{ 0x20, 0, 0, 0x0000001c },
{ 0x15, 2, 0, 0x0a15037c },
{ 0x20, 0, 0, 0x00000026 },
{ 0x15, 0, 1, 0x0a15037c },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },
组合代码形成第三版:
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <linux/ip.h>
// #include <linux/in.h>
#include <linux/if_ether.h>
#include <arpa/inet.h>
#include <netinet/udp.h>
#include <sys/ioctl.h>
#include <string.h>
#include <net/if.h>
#include <linux/if_packet.h>
#include <linux/filter.h>
#define BUF_LEN 2048
void print_mac(unsigned char *mac)
{
printf("mac: %02x:%02x:%02x:%02x:%02x:%02x", mac[6], mac[7], mac[8], mac[9], mac[10], mac[11]);
printf("-> ");
printf("%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
printf("\n");
}
void print_ip(unsigned char *ip)
{
struct iphdr *iph = (struct iphdr *)ip;
if (iph->version == 4)
{
printf("ip: %d.%d.%d.%d ",
iph->saddr & 0xFF, (iph->saddr >> 8) & 0xFF,
(iph->saddr >> 16) & 0xFF, (iph->saddr >> 24) & 0xFF);
printf("-> ");
printf("%d.%d.%d.%d\n",
iph->daddr & 0xFF, (iph->daddr >> 8) & 0xFF,
(iph->daddr >> 16) & 0xFF, (iph->daddr >> 24) & 0xFF);
}
printf("\n");
}
int main(int argc, char *argv[])
{
char buf[BUF_LEN];
unsigned char *ipheader, *etheader;
struct packet_mreq sock_params;
struct sock_filter BPF_CODE[] = {
{0x28, 0, 0, 0x0000000c},
{0x15, 0, 4, 0x00000800},
{0x20, 0, 0, 0x0000001a},
{0x15, 8, 0, 0x0a15037c},
{0x20, 0, 0, 0x0000001e},
{0x15, 6, 7, 0x0a15037c},
{0x15, 1, 0, 0x00000806},
{0x15, 0, 5, 0x00008035},
{0x20, 0, 0, 0x0000001c},
{0x15, 2, 0, 0x0a15037c},
{0x20, 0, 0, 0x00000026},
{0x15, 0, 1, 0x0a15037c},
{0x6, 0, 0, 0x00040000},
{0x6, 0, 0, 0x00000000}
};
struct sock_fprog filter;
filter.len = sizeof(BPF_CODE) / sizeof(BPF_CODE[0]);
filter.filter = BPF_CODE;
int sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sockfd < 0)
{
perror("socket");
return -1;
}
// 获取网卡号
struct ifreq req;
char *eth_name = "em2";
strncpy(req.ifr_name, eth_name, strlen(eth_name) + 1);
int ret = ioctl(sockfd, SIOCGIFINDEX, &req);
if (ret < 0)
{
perror("ioctl");
return -1;
}
struct sockaddr_ll addr;
addr.sll_family = AF_PACKET;
addr.sll_ifindex = req.ifr_ifindex;
addr.sll_protocol = htons(ETH_P_ALL);
// 采用这种方式设置的混杂模式才能绑定成功
memset(&sock_params, 0, sizeof(sock_params));
sock_params.mr_type = PACKET_MR_PROMISC;
sock_params.mr_ifindex = addr.sll_ifindex;
ret = setsockopt(sockfd,SOL_PACKET, PACKET_ADD_MEMBERSHIP, (void *)&sock_params, sizeof(sock_params));
if (ret < 0)
{
perror("setsockopt promisc");
return -1;
}
// 过滤
ret = setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter));
if (ret < 0)
{
perror("setsockopt filter");
return -1;
}
// 绑定
ret = bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
if (ret < 0)
{
perror("bind");
return -1;
}
while (1)
{
ret = recvfrom(sockfd, buf, BUF_LEN, 0, NULL, NULL);
if (ret < 0)
{
perror("recvfrom");
return -1;
}
printf("recv %d bytes\n", ret);
if (ret < sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr))
{
continue;
}
etheader = buf;
printf("--------------one packet--------------\n");
print_mac(etheader);
ipheader = buf + sizeof(struct ethhdr);
if (((struct ethhdr *)etheader)->h_proto == htons(ETH_P_IP))
{
print_ip(ipheader);
if (((struct iphdr *)ipheader)->protocol == IPPROTO_UDP)
{
printf("proto:UDP\n");
}
else if (((struct iphdr *)ipheader)->protocol == IPPROTO_TCP)
{
printf("proto:TCP\n");
}
}
printf("-------------------------------------\n\n");
}
close(sockfd);
return 0;
}
打印结果:
--------------one packet--------------
mac: 00:e0:4c:68:05:91-> ff:ff:ff:ff:ff:ff
ip: 10.21.3.124 -> 10.21.71.246
proto:UDP
-------------------------------------
recv 92 bytes
--------------one packet--------------
mac: 00:e0:4c:68:05:91-> ff:ff:ff:ff:ff:ff
ip: 10.21.3.124 -> 10.21.71.246
proto:UDP
-------------------------------------
recv 92 bytes
--------------one packet--------------
mac: 00:e0:4c:68:05:91-> ff:ff:ff:ff:ff:ff
ip: 10.21.3.124 -> 10.21.71.246
proto:UDP
3.2 AF_SOCKET高级点的应用
3.2.1 AF_SOCKET 性能改进
AF_SOCKET 上述的方式,性能不高,原因有几个:
- 如果连续收包,我们需要每次都要申请内存,在内存申请时候,可能会发生丢包;
- 数据包需要从内核缓冲区拷贝到应用的缓冲区,浪费。
-
在收包的时候,每个recv都需要一次系统调用,导致上下文切换,这个速度也快不起来。
提升办法采用PACKET_MMAP,PACKET_MMAP 会申请一块缓存环,这个缓存会在内核和用户应用程序之间共享,所以就不用来回进行数据拷贝。在发送的时候,可以一次系统调用发送多个包。另外也不需要通过recv进行一次系统调用,只需要调用poll准备好了就可以直接获取环形缓冲区的数据了。
收包的使用步骤如下:
PACKET_MMP申请的是一块环形缓冲区,缓冲区由多个block组成,每个block是一块物理上连续的内存区域,按照页面大小对齐,即必须是页面大小的整数倍。每个frame必须放在一个block中,每个block保存整数个frame。每个block可以存放tp_block_size/tp_frame_size个数据frame。block的总数是tp_block_nr,每个frame包含frame头和数据包数据。
PACKET_MMP 分为三个版本:
- TPACKET_V1
- TPACKET_V2
时间戳分辨率为纳秒级,而不是微秒级。
数据包具有 VLAN 元数据信息。- TPACKET_V3
读取/轮询是在块级别而不是帧级别。
添加轮询超时以避免阻塞轮询。
RX 哈希数据可供用户空间应用程序使用
建议使用版本是TPACKET_V3,因为块级轮询可带来 CPU 使用率降低 15% - 20% 和数据包捕获率提高约 20% 的好处。
设置V3版本的方法(需要内核支持才行):
int v = TPACKET_V3;
err = setsockopt(fd, SOL_PACKET, PACKET_VERSION, &v, sizeof(v));
要为 RX 和 TX 设置环,TPACKET_V1 和 TPACKET_V2 使用struct tpacket_req,而 TPACKET_V3 使用struct tpacket_req3,两个结构均在 中定义uapi/linux/if_packet.h
。以下代码设置了PACKET_RX_RING128 个块,每个块有 4096 字节,包含 2 个帧,帧大小为 2048 字节。
数据结构如下:
/*备注:tpacket_req3结构是tpacket_req结构的超集,实际可以统一使用本结构去设置所有版本的环形缓冲区,V1/V2版本会自动忽略多余的字段*/
struct tpacket_req
{
unsigned int tp_block_size;/*连续块的最小大小*/
unsigned int tp_block_nr;/*数据块数量*/
unsigned int tp_frame_size;/*帧的大小*/
unsigned int tp_frame_nr;/*总帧数*/
};
struct tpacket_req3 {
unsigned int tp_block_size; // 每个连续内存块的最小尺寸(必须是 PAGE_SIZE * 2^n )
unsigned int tp_block_nr; // 内存块数量
unsigned int tp_frame_size; // 每个帧的大小(虽然V3中的帧长是可变的,但创建时还是会传入一个最大的允许值)
unsigned int tp_frame_nr; // 帧的总个数(必须等于 每个内存块中的帧数量*内存块数量)
unsigned int tp_retire_blk_tov; // 内存块的寿命(ms),超时后即使内存块没有被数据填入也会被内核停用,0意味着不设超时
unsigned int tp_sizeof_priv; // 每个内存块中私有空间大小,0意味着不设私有空间
unsigned int tp_feature_req_word;// 标志位集合(目前就支持1个标志 TP_FT_REQ_FILL_RXHASH)
}
// TPACKET_V3环形缓冲区每个帧的头部结构
struct tpacket3_hdr {
__u32 tp_next_offset; // 指向同一个内存块中的下一个帧
__u32 tp_sec; // 时间戳(s)
__u32 tp_nsec; // 时间戳(ns)
__u32 tp_snaplen; // 捕获到的帧实际长度
__u32 tp_len; // 帧的理论长度
__u32 tp_status; // 帧的状态
__u16 tp_mac; // 以太网MAC字段距离帧头的偏移量
__u16 tp_net;
union {
struct tpacket_hdr_variant1 hv1; // 包含vlan信息的子结构
};
__u8 tp_padding[8];
}
struct tpacket_req3 req;
req.tp_block_size = 4096;
req.tp_frame_size = 2048;
req.tp_block_nr = 128;
req.tp_frame_nr = (req.tp_block_size * req.tp_block_nr) / req.tp_frame_size;
err = setsockopt(fd, SOL_PACKET, PACKET_RX_RING, &req, sizeof(req));
如果设置发送缓存,方法差不多。接着就是申请mmap映射内存了,如下:
unsigned int total_size = req.tp_block_size * req.tp_block_nr;
ring = mmap(NULL, total_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
mmap申请total_size大小的内存,返回指向缓冲区的指针。
第一个参数:指定共享缓冲区的起始地址,如果是NULL,内核将选择创建映射的地址。
第二个参数:指定共享缓冲区的总大小。
第三个参数:PROT_READ|PROT_WRITE第三个参数表示映射空间是否可读可写。
第四个参数的标志决定映射的更新是否对映射相同内存空间的其他进程可见。
最后一个参数是 AF_PACKET 映射环缓冲区的偏移量,始终设置为 0。
内核和应用程序共享这块缓冲区,为了能相互感知,需要有状态标志,主要有以下几种:
#define TP_STATUS_KERNEL 0
#define TP_STATUS_USER 1
#define TP_STATUS_COPY (1 << 1)
#define TP_STATUS_LOSING (1 << 2)
#define TP_STATUS_CSUM_VALID (1 << 7)
内核将所有的frame初始化为TP_STATUS_KERNEL,内核收到数据包后,放入到缓存中,将frame的状态标记为TP_STATUS_USER,一旦读取数据包,应用程序必须将状态字段清零,这样内核就可以重用该帧缓冲区来存储下一个收到的数据包。
TP_STATUS_COPY : 表示帧(及相关元数据)已被截断,因为它大于tp_frame_size。可以使用 完整读取此数据包recvfrom()。但是,为了使其工作,必须先使用setsockopt()和PACKET_COPY_THRESH选项启用它。
TP_STATUS_LOSING:表示上次检查统计信息时有数据包丢失getsockopt()以及PACKET_STATISTICS选项。
TP_STATUS_CSUM_VALID: 此标志表示至少数据包的传输头校验和已在内核端验证。如果未设置此标志,则用户空间应用程序可以自由检查校验和(前提是TP_STATUS_CSUMNOTREADY也未设置此标志)。
发送和接数据包过程如下:
接收数据包:内核收到数据包后将其存入接收环形缓冲区中,poll( )轮询到有数据包后,用户层根据frame的状态(TP_STATUS_USER)判断数据包能否处理。若进行处理,则将对应状态由TP_STATUS_USER改为TP_STATUS_KERNEL告诉内核这块缓冲区对应的数据包已经被处理,可以继续存放新的数据包;
发送数据包:用户产生需要发送的数据包后,从发送环形缓冲区遍历寻找一个可用状态(TP_STATUS_AVAILABLE)的frame将数据包存入后,状态置为TP_STATUS_SEND_REQUEST。通过poll( )轮询发送缓冲区,当有需要发送的数据包时,通过sendto( )函数提醒内核从缓冲区进行发送。
以V1版本为准,举个例子:
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <linux/ip.h>
// #include <linux/in.h>
#include <linux/if_ether.h>
#include <arpa/inet.h>
#include <netinet/udp.h>
#include <sys/ioctl.h>
#include <string.h>
#include <net/if.h>
#include <linux/if_packet.h>
#include <linux/filter.h>
#include <sys/mman.h>
#include <poll.h>
#include <stdlib.h>
#define BUF_LEN 2048
#define PER_PACKET_SIZE 2048
#ifndef __aligned_tpacket
# define __aligned_tpacket __attribute__((aligned(TPACKET_ALIGNMENT)))
#endif
#ifndef __align_tpacket
# define __align_tpacket(x) __attribute__((aligned(TPACKET_ALIGN(x))))
#endif
union frame_map {
struct {
struct tpacket_hdr tp_h __aligned_tpacket;
struct sockaddr_ll s_ll __align_tpacket(sizeof(struct tpacket_hdr));
} *v1;
struct {
struct tpacket2_hdr tp_h __aligned_tpacket;
struct sockaddr_ll s_ll __align_tpacket(sizeof(struct tpacket2_hdr));
} *v2;
void *raw;
};
void print_mac(unsigned char *mac)
{
printf("mac: %02x:%02x:%02x:%02x:%02x:%02x", mac[6], mac[7], mac[8], mac[9], mac[10], mac[11]);
printf("-> ");
printf("%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
printf("\n");
}
void print_ip(unsigned char *ip)
{
struct iphdr *iph = (struct iphdr *)ip;
if (iph->version == 4)
{
printf("ip: %d.%d.%d.%d ",
iph->saddr & 0xFF, (iph->saddr >> 8) & 0xFF,
(iph->saddr >> 16) & 0xFF, (iph->saddr >> 24) & 0xFF);
printf("-> ");
printf("%d.%d.%d.%d\n",
iph->daddr & 0xFF, (iph->daddr >> 8) & 0xFF,
(iph->daddr >> 16) & 0xFF, (iph->daddr >> 24) & 0xFF);
}
printf("\n");
}
void dealPacket(char * buf)
{
unsigned char * etheader = buf;
printf("--------------one packet--------------\n");
print_mac(etheader);
unsigned char * ipheader = ipheader = buf + sizeof(struct ethhdr);
if (((struct ethhdr *)etheader)->h_proto == htons(ETH_P_IP))
{
print_ip(ipheader);
if (((struct iphdr *)ipheader)->protocol == IPPROTO_UDP)
{
printf("proto:UDP\n");
}
else if (((struct iphdr *)ipheader)->protocol == IPPROTO_TCP)
{
printf("proto:TCP\n");
}
}
else {
//printf("proto:%d other\n", (struct ethhdr *)etheader->h_proto );
}
printf("-------------------------------------\n\n");
}
int main(int argc, char *argv[])
{
char buf[BUF_LEN];
unsigned char *ipheader, *etheader;
struct packet_mreq sock_params;
int sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sockfd < 0)
{
perror("socket");
return -1;
}
// 获取网卡号
struct ifreq req;
char *eth_name = "em1";
strncpy(req.ifr_name, eth_name, strlen(eth_name) + 1);
int ret = ioctl(sockfd, SIOCGIFINDEX, &req);
if (ret < 0)
{
perror("ioctl");
return -1;
}
const int tpacket_version = TPACKET_V1;
/* set tpacket hdr version. */
ret = setsockopt(sockfd, SOL_PACKET, PACKET_VERSION, &tpacket_version, sizeof(int));
if (ret < 0)
{
perror("setsockopt");
return -1;
}
struct tpacket_req tp_req;
const int BUFFER_SIZE = 1024 * 1024 * 16;
tp_req.tp_block_size = 4096;
tp_req.tp_block_nr = BUFFER_SIZE / tp_req.tp_block_size;
tp_req.tp_frame_size = PER_PACKET_SIZE;
tp_req.tp_frame_nr = (tp_req.tp_block_size * tp_req.tp_block_nr ) / tp_req.tp_frame_size;
int mem_len = BUFFER_SIZE;
ret = setsockopt(sockfd, SOL_PACKET, PACKET_RX_RING, (void *)&tp_req, sizeof(tp_req));
if (ret < 0)
{
perror("setsockopt");
return -1;
}
char *buff = (char *)mmap(0, BUFFER_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, sockfd, 0);
if (buff == MAP_FAILED)
{
perror("mmap");
return -1;
}
memset(buff, 0, BUFFER_SIZE);
// 相当于环形数组 循环使用
struct iovec *rd;
// 每个frame需要一个对应的iov
int rd_len = tp_req.tp_frame_nr* sizeof(struct iovec);
rd = (struct iovec *)malloc(rd_len);
for (int i = 0; i< tp_req.tp_frame_nr; i++) {
rd[i].iov_base = buff + i * tp_req.tp_frame_size;
rd[i].iov_len = tp_req.tp_frame_size;
}
struct sockaddr_ll addr;
addr.sll_family = AF_PACKET;
addr.sll_ifindex = req.ifr_ifindex;
addr.sll_protocol = htons(ETH_P_ALL);
// 采用这种方式设置的混杂模式才能绑定成功
memset(&sock_params, 0, sizeof(sock_params));
sock_params.mr_type = PACKET_MR_PROMISC;
sock_params.mr_ifindex = addr.sll_ifindex;
ret = setsockopt(sockfd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, (void *)&sock_params, sizeof(sock_params));
if (ret < 0)
{
perror("setsockopt promisc");
return -1;
}
// 绑定
ret = bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
if (ret < 0)
{
perror("bind");
return -1;
}
int index = 0;
union frame_map ppd;
while (1)
{
ppd.raw = rd[index].iov_base;
const unsigned int tp_status = ppd.v1->tp_h.tp_status;
// ready
if ( (tp_status & TP_STATUS_USER) == TP_STATUS_USER)
{
goto PROCESS;
}
struct pollfd pfd;
pfd.fd = sockfd;
pfd.events = POLLIN;
pfd.revents = 0;
ret = poll(&pfd, 1, -1);
if (ret < 0)
{
perror("poll");
return -1;
}
PROCESS:
for ( ; index < tp_req.tp_frame_nr; )
{
ppd.raw = rd[index].iov_base;
const unsigned int tp_status = ppd.v1->tp_h.tp_status;
/* if ( (tp_status & TP_STATUS_KERNEL) == TP_STATUS_KERNEL)
{
break;
}*/
dealPacket((char*)ppd.raw + ppd.v1->tp_h.tp_mac);
ppd.v1->tp_h.tp_len = 0;
ppd.v1->tp_h.tp_status = TP_STATUS_KERNEL;
index += 1;
index %= tp_req.tp_frame_nr;
}
}
close(sockfd);
munmap(buff, BUFFER_SIZE);
return 0;
}
为了简化代码,去掉了过滤功能部分的代码。
结构说明:
代码参考内核测试代码,mmap申请的内存保存真实的数据,另外申请一个struct iovec
指针数组方便操作,指向tp_req。
参考
https://man7.org/linux/man-pages/man7/packet.7.html
https://www.kernel.org/doc/Documentation/networking/packet_mmap.txt
https://csulrong.github.io/blogs/2022/03/10/linux-afpacket/
https://kernelnewbies.org/Networking?action=AttachFile&do=get&target=hacking_the_wholism_of_linux_net.txt
内核源码案例:https://github.com/torvalds/linux/blob/master/tools/testing/selftests/net/psock_tpacket.c#L66