linux 原始套接字收发 packet

近期工作接触到了些 linux 在 OSI 二层(数据链路层)使用 ethernet 协议收发数据包的内容,网上搜罗了一些有用的文档:

另外有一些第三方库也可以做到处理链路层数据:

下面是我对 linux man 手册 packet(7) 的翻译,虽然上面已有相关中文翻译的链接,但我个人认为有部分相关术语使用得不太合适,另外我自己再翻译一边也可以加深我个人理解。

NAME

packet - 设备层的 packet 接口

SYNOPSIS 概要

1
2
3
4
5
#include <sys/socket.h>
#include <linux/if_packet.h>
#include <net/ethernet.h> /* the L2 protocols */

packet_socket = socket(AF_PACKET, int socket_type, int protocol);

DESCRIPTION 描述

如使用如下调用创建的 socket 被称为 packet socket:

1
packet_socket = socket(AF_PACKET, int socket_type, int protocol);

packet sockets 被用来在设备驱动层(OSI 2 层 数据链路层)收发数据包(raw packets)。这允许用户在 OSI 1 层 物理层之上实现 linux 用户空间的协议模块。

socket_type 参数可以使用 SOCK_RAW 或者 SOCK_DGRAM,使用 SOCK_RAW 时收发的数据包 包含 链路层的协议头(译者注:通常是以太网链路层协议头),使用 SOCK_DGRAM 时收发的原始数据包 不包含 链路层协议头。链路层协议头相关信息可以从结构体 sockaddr_ll 中获取到。

protocol 参数是 网络字节序(大端) 的 IEEE 802.3 协议编号。相关定义可以在 <linux/if_ether.h> 头文件中查找。当 protocol 参数使用 htons(ETH_P_ALL) 时,相当于接收所有协议的数据包。protocol 参数指定的所有数据包会先给到用户创建的 socket,然后才由内核相关协议栈程序处理(译者注:也就是说用户实现的数据包处理程序,优先级高于内核,但不等于内核不再继续处理)。

在用户空间创建这种类型的 socket 时,进程必须拥有 CAP_NET_RAW 的能力以便管理 network namespace(译者注:这里是在说运行程序时需要一定的权限,或者直接使用 root 账户运行)。

使用 SOCK_RAW 参数时,数据包会从设备驱动不经任何修改得被程序接收和发送。当接收一个数据包时,address 仍被解析并设置到结构体 sockaddr_ll 中。当发送一个数据包时,用户提供的 buffer 需要包含物理层协议头(译者注:用户要发送的数据中需要包含以太网协议头,同时还需要在 sockaddr_ll 中设置以太网协议头中已经包含了的目标 MAC 地址)。数据包不经修改得被放网络接口的驱动队列中,网络接口由目标 address 定义(译者注:sockaddr_ll 结构体的一个成员)。SOCK_RAW 类似但不兼容 Linux 2.0 时使用的参数 AF_INET + SOCK_PACKET。

使用 SOCK_DGRAM 参数时相当于处于一个较高层次。接收数据包时,以太网协议头将会被移除。发送数据包时,会自动从 sockaddr_ll 中提取的以太网协议头信息进行封包。

默认情况下,所有 protocol 参数指定的协议类型的数据包,都会给到用户创建的 socket。如果需要只获取某个网络接口上的数据包,可以使用 bind(2) 函数,将指定的 socket 和 sockaddr_ll 传入作为参数,sockaddr_ll 中需要设置的字段有 sll_family(使用 AF_PACKET),sll_protocol 和 sll_ifindex(指定的网络接口)。

这种类型的 socket 不支持 connect 操作。

当调用 recvmsg(2) recv(2) 或 recvfrom(2) 时,如果指定了 MSG_TRUNC flag,将会返回数据包的真实大小,即使数据包的大小比参数 buffer 要大。

Address types

结构体 sockaddr_ll 是设备无关的,其定义如下:

1
2
3
4
5
6
7
8
9
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 */
};

结构体成员相关含义如下(译者注:并非在所有使用场景下,都需要给所有的成员变量赋值,比如上面提到的调用 bind(2) 函数时,只需要设置 3 个成员即可):

  • sll_family 始终应该是 AF_PACKT
  • ssl_protocol 是 <linux/if_ether.h> 头文件中定义的标准的 ethernet protocol 类型,且使用网络序(大端),默认是 socket 的 protocol(译者注:这里的意思应该是每种 socket_type 都有其默认的 protocol,具体不清楚)(译者注:使用此头文件中未定义的值也可以,比如 Profinet 的 0x8892 就没有在头文件中定义,但仍可以使用)
  • sll_ifindex 是网络接口的索引号,参见 netdevice(7),值为 0 时表示 any 接口(只允许执行 bind 操作)。
  • sll_hatype 是 ARP 类型,参见 <linux/if_arp.h> 头文件。
  • sll_pkttype 是数据包类型,可选的值有:
    • PACKET_HOST 发送到 local host 的数据包
    • PACKET_BROADCAST 以太网广播数据包(译者注:以太网协议头的目标 MAC 地址为全 0xFF)
    • PACKET_MULTICAST 以太网多播数据包
    • PACKET_OTHERHOST 发送到其他机器的数据包,且此数据包已经被设备驱动在 promiscuous 模式下捕获
    • PACKET_OUTGOING 来自本地主机的数据包,该数据包被环回到数据包套接字
    • 这些类型都只在接收数据时有效(译者注:这个字段应该是只在接收数据时其作用)
  • sll_halen 地址数据的长度
  • sll_addr 地址(译者注:以太网协议时,是 MAC 地址)当发送数据包时,只需要设置 sll_family + sll_protocol + sll_ifindex + sll_addr + sll_halen 即可,其他字段应该是 0。其他字段在接收数据时需要设置。

后续内容未经过完整翻译

Socket options

socket options 通过调用 setsockopt(2) 设置,level 参数应该是 SOL_PACKET

  • PACKET_ADD_MEMBERSHIP / PACKET_DROP_MEMBERSHIP
    packet sockets 可以被用来设置物理层/链路层多播和 promiscuous 模式(传统的 ioctls SIOCSIFFLAGS SIOCADDMULTI SIOCDELMULTI 也可以实现相同的目的)。PACKET_ADD_MEMBERSHIP 用来新增配置,相对应的 PACKET_DROP_MEMBERSHIP 则用来移除配置。它们都需要结构体 packet_mreq 作为参数,其定义如下:
    1
    2
    3
    4
    5
    6
    struct packet_mreq {
    int mr_ifindex; /* interface index */
    unsigned short mr_type; /* action */
    unsigned short mr_alen; /* address length */
    unsigned char mr_address[8]; /* physical-layer address */
    };
    此结构体成员含义如下:
    • mr_ifindex 根据网络接口索引号,指定将要修改哪个网络接口
    • mr_type 执行要执行什么操作,其可选值如下:
      • PACKET_MR_PROMISC 表示允许接收共享介质上的所有数据包(即: promiscuous mode)
      • PACKET_MR_MULTICAST 设置 multicast group
      • PACKET_MR_ALLMULTI 表示接收此网络接口上的 all multicast packets
    • mr_alen / mr_address 用来在 mr_type 为 PACKET_MR_MULTICAST 时,设置 multicast group 相关信息
  • PACKET_AUXDATA (since Linux 2.6.21)
    If this binary option is enabled, the packet socket passes a metadata structure along with each packet in the recvmsg(2) control field. The structure can be read with cmsg(3). It is defined as
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct tpacket_auxdata {
    __u32 tp_status;
    __u32 tp_len; /* packet length */
    __u32 tp_snaplen; /* captured length */
    __u16 tp_mac;
    __u16 tp_net;
    __u16 tp_vlan_tci;
    __u16 tp_vlan_tpid; /* Since Linux 3.14; earlier, these
    were unused padding bytes */
    };
  • PACKET_FANOUT (since Linux 3.1)
  • PACKET_RESERVE (with PACKET_RX_RING)
  • PACKET_RX_RING
  • PACKET_STATISTICS
  • PACKET_TIMESTAMP (with PACKET_RX_RING; since Linux 2.6.36)
  • PACKET_TX_RING (since Linux 2.6.31)
  • PACKET_VERSION (with PACKET_RX_RING; since Linux 2.6.27)
  • PACKET_QDISC_BYPASS (since Linux 3.14)

ioctls

SIOCGSTAMP 可以用来获取接收最后一个数据包的时间戳 timestamp。参数是结构体 timeval。

另外,所有在 netdevice(7) 和 socket(7) 中定义的标准 ioctls 在 packet sockets 上都是有效的。

Error handling

除了在将数据包传递给设备驱动程序时发生的错误之外,数据包套接字不进行错误处理。他们没有未决错误的概念。

ERRORS

  • EADDRNOTAVAIL Unknown multicast group address passed.
  • EFAULT User passed invalid memory address.
  • EINVAL Invalid argument.
  • EMSGSIZE Packet is bigger than interface MTU.
  • ENETDOWN Interface is not up.
  • ENOBUFS Not enough memory to allocate the packet.
  • ENODEV Unknown device name or interface index specified in interface address.
  • ENOENT No packet received.
  • ENOTCONN No interface address passed.
  • ENXIO Interface address contained an invalid interface index.
  • EPERM User has insufficient privileges to carry out this operation.

In addition, other errors may be generated by the low-level driver.

VERSIONS

AF_PACKET is a new feature in Linux 2.2. Earlier Linux versions
supported only SOCK_PACKET.

NOTES

对于可移植程序,建议通过 pcap(3) 使用 AF_PACKET;尽管这仅涵盖了 AF_PACKET 功能的一个子集。

The SOCK_DGRAM packet sockets make no attempt to create or parse the IEEE 802.2 LLC header for a IEEE 802.3 frame.
SOCK_DGRAM 数据包套接字不会尝试为 IEEE 802.3 帧创建或解析 IEEE 802.2 LLC header。

When ETH_P_802_3 is specified as protocol for sending the kernel creates the 802.3 frame and fills out the length field; the user has to supply the LLC header to get a fully conforming packet.
当 ETH_P_802_3 被指定为发送协议时,内核创建 802.3 帧并填写长度字段;用户必须自行提供 LLC header 才能获得完全符合要求的数据包。

Incoming 802.3 packets are not multiplexed on the DSAP/SSAP protocol fields; instead they are supplied to the user as protocol ETH_P_802_2 with the LLC header prefixed.
传入的 802.3 数据包不会在 DSAP/SSAP 协议字段上复用;相反,它们作为带有 LLC 标头前缀的协议 ETH_P_802_2 提供给用户。

It is thus not possible to bind to ETH_P_802_3; bind to ETH_P_802_2 instead and do the protocol multiplex yourself.
因此无法绑定到 ETH_P_802_3;而是绑定到 ETH_P_802_2 并自己进行协议复用。

The default for sending is the standard Ethernet DIX encapsulation with the protocol filled in.
发送的默认值是标准的以太网 DIX 封装,并填充了协议。

Packet sockets are not subject to the input or output firewall
chains.

Compatibility

In Linux 2.0, the only way to get a packet socket was with the
call:

1
socket(AF_INET, SOCK_PACKET, protocol)

This is still supported, but deprecated and strongly discouraged.
The main difference between the two methods is that SOCK_PACKET
uses the old struct sockaddr_pkt to specify an interface, which
doesn’t provide physical-layer independence.

1
2
3
4
5
struct sockaddr_pkt {
unsigned short spkt_family;
unsigned char spkt_device[14];
unsigned short spkt_protocol;
};

spkt_family contains the device type, spkt_protocol is the IEEE
802.3 protocol type as defined in <sys/if_ether.h> and
spkt_device is the device name as a null-terminated string, for
example, eth0.

This structure is obsolete and should not be used in new code.

BUGS

The IEEE 802.2/803.3 LLC handling could be considered as a bug.

Socket filters are not documented.

The MSG_TRUNC recvmsg(2) extension is an ugly hack and should be
replaced by a control message. There is currently no way to get
the original destination address of packets via SOCK_DGRAM.