AF_PACKET 链路层通信

更新时间:
2024-03-14
下载文档

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_IPIP 类型数据帧0x0800
ETH_P_ARPARP 类型数据帧0x0806
ETH_P_RARPRARP 类型数据帧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 函数等。

文档内容是否对您有所帮助?
有帮助
没帮助