1. Packet 套接字

文档地址:https://man7.org/linux/man-pages/man7/packet.7.html

1.1. 名称

packet - 设备级数据包接口。

1.2. 概要

1.3. 描述

Packet 套接字用于在设备驱动级别(OSI 二层)接收或发送原始数据包。它支持用户在用户空间在物理层上实现协议模块。

socket_type 要么为 SOCK_RAW(用于包含链路层头部的原始数据包),要么为 SOCK_DGRAM(用于移除链路层头部的 cooked 数据包)。sockaddr_ll 结构体是链路层头部信息的通用格式。

protocol 是网络字节序表示的 IEEE 802.3 协议号。查看 <linux/if_ether.h> 头文件,获取支持的协议列表。如果将协议设置为 htons(ETH_P_ALL),那么接收全部协议。在将进入的特定协议类型的数据包传递给内核实现的协议之前,数据包将被传递到 Packet 套接字。如果将 protocol 设置为 0,那么不接收任何数据包。可以使用非零的 sll_protocol 调用 bind(2),以开始接收指定协议的数据包。

如果想创建 Packet 套接字,那么进程在管理其网络命名空间的用户命名空间中,必须拥有 CAP_NET_RAW 能力。

向和从设备驱动传递 SOCK_RAW 数据包时,不会更改数据包数据。接收数据包时,仍然解析地址,并且通过标准的 sockaddr_ll 地址结构体传递。传输数据包时,用户提供的缓冲区应该包含物理层头部。然后将该数据包放进由目的地址指定的接口的网络驱动的队列中。一些设备驱动也添加其它头部。SOCK_RAW 与废弃的 Linux 2.0 AF_INET/SOCK_PACKET 相似,但不兼容。

SOCK_DGRAM 工作在稍高层次上。在将数据包传递给用户之前,移除物理头部。通过 SOCK_DGRAM Packet 套接字发送的数据包,在排队之前,将根据 sockaddr_ll 目的地址中的信息获得适当的物理层头部。

默认情况下,指定协议类型的全部数据包都被传递给 Packet 套接字。如果想仅从特定的接口获取数据包,那么使用在 struct sockaddr_ll 中指定地址的 bind(2) ,将 Packet 套接字绑定到接口上。用于绑定的字段包括 sll_family(应为 AF_PACKET)、sll_protocolsll_ifindex

Packet 套接字不支持 connect(2) 操作。

当将 MSG_TRUNC 标记传递给 recvmsg(2)、recv(2) 或 recvfrom(2) 时,无论数据包的长度是否超过缓冲区,始终返回其在链路上的实际长度。

1.3.1. 地址类型

sockaddr_ll 结构体是与设备无关的物理层地址。

该结构体的字段如下所示:

sll_protocol

以网络字节顺序表示的标准以太网协议类型,其定义在 <linux/if_ether.h> 头文件中。它默认为套接字的协议。

sll_ifindex

接口的接口索引(参考 netdevice(7));0 表示匹配任何接口(仅允许绑定)。

sll_hatype

<linux/if_arp.h> 头文件中定义的 ARP 类型

sll_pkttype

包含数据包类型。有效类型包括:

这些类型只在接收时有意义。

sll_addr

sll_halen

包含物理层(比如,IEEE 802.3)地址及其长度。具体解释取决于设备。

发送数据包时,需要指定 sll_familysll_addrsll_halensll_ifindexsll_protocol。其它字段应该为 0。sll_hatypesll_pkttype 是在接收到的数据包上设置的,用于提供信息。

1.3.2. 套接字选项

通过使用级别 SOL_PACKET 调用 setsockopt(2) 的方式,配置 Packet 套接字选项。

1.3.2.1. PACKET_FANOUT

为在多个线程之间扩展处理能力,Packet 套接字可以构成扇出(fanout)组。在该模式中,每个匹配的数据包只被放入组内的一个套接字的队列中。通过使用级别 SOL_PACKET 和选项 PACKET_FANOUT 调用 setsockopt(2) 的方式,将套接字加入扇出组。每个网络命名空间最多可以拥有 65536 个独立的组。套接字通过在整型选项值的前 16 位中编码 ID 的方式,选择组。第一个加入组的 Packet 套接字隐式地创建组。如果想加入已存在的组,后续的 Packet 套接字必须使用相同的协议、设备设置、扇出模式和标记。Packet 套接字只能通过关闭套接字的方式离开扇出组。当最后一个套接字关闭时,组被删除。

扇出支持多种算法在套接字间分发流量,如下所示:

扇出模式可以使用其它选项。IP 分片导致同一流量的数据包具有不同的流哈希值。如果设置标记 PACKET_FANOUT_FLAG_DEFRAG,那么在应用扇出之前,将对数据包进行 defragmentation(重新组装),以保留顺序。扇出模式和选项通过整型选项值的第二个 16 位进行传递。标记 PACKET_FANOUT_FLAG_ROLLOVER 启用回滚机制作为备用策略:如果原始扇出算法选择的是积压套接字,那么数据包将滚动到下一个可用的套接字。


2. 示例

2.1. Cargo.toml

2.2. src/lib.rs

2.3. src/main.rs


附录 1 - 结构体及函数

1. packet_mreq

packet_mreq 是在 C 网络编程中使用的结构体,用于设置和配置网络套接字的多播(Multicast)参数。

在 POSIX 操作系统(比如 Linux、Unix)中,多播是一种网络通信模式,允许一个发送方将数据包发送给一组接收方,而不是仅发送给单个目标。多播通常用于在局域网或广域网上同时向多个主机传送数据。

packet_mreq 结构体定义如下:

通过使用 packet_mreq 结构体,可以设置和配置网络套接字,以加入或离开指定的多播组,以及指定网络接口进行多播通信。

2. libc::ifaddrs

libc::ifaddrs 用于表示网络接口的地址信息。它是与 C 标准库进行交互时,使用的类型。

以下是 libc::ifaddrs 结构体的定义:

3. libc::sockaddr

libc::sockaddr 结构体用于表示通用的套接字地址。

以下是 libc::sockaddr 结构体的定义:

sockaddr 结构体共 16 个字节,是 Socket 编程中的标准地址结构体。几乎所有 Socket API 都使用 sockaddr 作为其地址信息存储结构。但由于定义它时还处于 IPv4 年代,没有预料到 IPv6 的诞生,sockaddr 大小只有 16 字节,无法存储 IPv6 128 位的 IP 地址,因此引入第二种通用地址结构 sockaddr_storage

4. libc::getifaddrs

libc::getifaddrs 函数用于获取当前系统上可用网络接口的地址信息列表。

以下是 libc::getifaddrs 函数的签名:

该函数接受指向 libc::ifaddrs 结构体指针的指针作为参数,返回整数值。

5. libc::sockaddr_storage

libc::sockaddr_storage 结构体提供通用的套接字地址存储结构,可以用于容纳各种类型的套接字地址。根据不同的地址家族,套接字地址的具体信息可能存储在 sockaddr_storage 结构体的其它字段中。比如,对于 IPv4 地址家族,可以将 sockaddr_in 结构体的实例强制转换为 sockaddr_storage 类型进行存储和操作。

以下是 libc::sockaddr_storage 结构体的定义:

6. libc::sockaddr_in

libc::sockaddr_in 结构体用于表示 IPv4 套接字地址。

以下是 libc::sockaddr_in 结构体的定义:

7. libc::fd_set

libc::fd_set 结构体用于表示文件描述符集合。

以下是 libc::fd_set 结构体的定义:

该结构体包含一个名为 fds_bits 的字段,它是固定大小的数组。该数组的长度根据 FD_SETSIZE 宏的值动态计算,以适应文件描述符集的大小。

通过设置或清除 fds_bits 数组中的相应位,向集合中添加或删除文件描述符。

8. libc::pselect

libc::pselect 函数用于在指定的文件描述符集上,进行阻塞式选择操作。

libc::pselect 函数的定义如下:

该函数接受以下参数:

libc::pselect 函数将阻塞程序的执行,直到满足以下条件之一:

在函数返回时,可以根据返回值确定选择操作的结果。如果返回值为负数,那么表示选择操作出错;如果返回值为 0,那么表示选择操作超时;如果返回值大于 0,那么表示就绪的文件描述符数量。

9. libc::timespec

libc::timespec 是在 Rust 中表示时间的结构体,通常用于与系统调用、时间相关的函数和库一起使用。

timespec 结构体定义一个时间点,包含两个字段:

其中,tv_sec 字段是 time_t 类型的整数,用于表示秒数。time_t 是长整型,通常为 32 位或 64 位整数,用于表示自 1970 年 1 月 1 日 00:00:00 UTC 起经过的秒数。

tv_nsec 字段是 c_long 类型的长整数,用于表示剩余的纳秒数。它的取值范围是 0 到 999,999,999。

通过结合 tv_sectv_nsec 字段,timespec 结构体可以表示精确到纳秒级别的时间点。

10. libc::sendto

libc::sendto 函数用于向指定的目的地址发送数据。

libc::sendto 函数的定义如下:

该函数接受以下参数:

libc::sendto 函数将指定的数据发送到目的地址。从缓冲区 buf 中读取待发送数据,长度为 len 字节。可以通过设置 flags 参数控制发送操作的行为,比如设置非阻塞模式、启用发送超时等。

在函数返回时,可以根据返回值确定发送操作的结果。如果返回值为负数,那么表示发送操作出错;如果返回值为零或正数,那么表示成功发送的字节数。

11. libc::recvfrom

libc::recvfrom 函数用于从指定的源地址接收数据。

libc::recvfrom 函数的定义如下:

该函数接受以下参数:

libc::recvfrom 函数从指定的套接字接收数据,将接收到的数据存储到缓冲区 buf 中,最多存储 len 字节的数据。可以通过设置 flags 参数,控制接收操作的行为,比如设置非阻塞模式、启用接收超时等。

在函数返回时,可以根据返回值确定接收操作的结果。如果返回值为负数,那么表示接收操作出错;如果返回值为零或正数,那么表示成功接收的字节数。

同时,src_addraddrlen 参数用于获取发送方的地址信息。src_addr 指向的 sockaddr 结构体将被填充为发送方的地址信息,addrlen 用于指定 src_addr 结构体的长度。

12. libc::fcntl

libc::fcntl 函数用于在文件描述符上执行各种控制操作。

libc::fcntl 函数的定义如下:

该函数接受以下参数:

比如 libc::fcntl(socket, libc::F_SETFL, libc::O_NONBLOCK) 将套接字(socket)设置为非阻塞模式。


附录 2 - 文件描述符

内核(Kernel)利用文件描述符(File Descriptor)访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核将返回一个文件描述符。读写文件时,也需要使用文件描述符指定待读写的文件。实际上,它是一个索引值,指向内核为每个进程所维护的该进程打开的文件的记录表。


附录 3 - strace tcpdump

可以在输出中看到:

可见在 Linux 下,tcpdump 是基于 Packet 套接字的。


See Also