4. 原始套接字(Raw Socket)入门

原始套接字是 Linux 网络编程中绕过内核协议栈部分处理、直接与网络层或链路层交互的套接字类型。本章从标准套接字与原始套接字的对比出发,重点讲解 AF_PACKET 域下 SOCK_RAW 的创建方法,并通过捕获链路层数据帧的实例,帮助读者建立对原始套接字工作模型的具体认知。

4.1 标准套接字 vs 原始套接字

标准套接字(如 SOCK_STREAM、SOCK_DGRAM)依赖内核协议栈完成头部封装/解析、路由查找、分片重组等操作。原始套接字则允许用户空间程序直接读写协议头部,甚至构造自定义协议帧。

对比维度 标准套接字 原始套接字
协议栈参与度 完整参与(TCP/UDP 头部由内核处理) 仅参与部分(用户可自定义头部)
数据帧访问范围 仅应用层载荷 可访问链路层、网络层头部
创建方式 socket(AF_INET, SOCK_STREAM, 0) socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))
权限要求 普通用户 需要 CAP_NET_RAW 或 root
典型用途 HTTP、FTP、WebSocket 抓包工具、自定义协议、网络诊断

关键区别:标准套接字是“应用层视角”,原始套接字是“协议栈视角”。使用原始套接字时,开发者需要自行处理以太网帧头、IP 头、TCP/UDP 头的构造与解析。

4.2 AF_PACKET 域与 SOCK_RAW 类型

AF_PACKET 是 Linux 特有的地址族,专门用于在链路层收发数据帧。结合 SOCK_RAW 类型,应用程序可以直接读取或注入完整的链路层帧(包括以太网头部)。

  • 协议参数:htons(ETH_P_ALL) 表示接收所有以太网协议类型的数据帧;也可以指定 ETH_P_IP、ETH_P_ARP 等过滤特定协议。
  • 数据帧结构:从 recvfrom() 读取的缓冲区包含完整的以太网帧(14 字节头部 + 载荷)。
  • 发送帧:使用 sendto() 时,需要自行构造完整的以太网帧,包括目标 MAC、源 MAC 和 EtherType。

提示:AF_PACKET + SOCK_RAW 是 tcpdump、Wireshark 等抓包工具在 Linux 上的底层实现基础。理解这一组合,就掌握了网络监控工具的“心脏”。

4.3 原始套接字创建与链路层帧捕获

下面通过一个完整的示例展示如何创建原始套接字并捕获链路层数据帧。代码中包含了必要的错误处理和帧头解析注释。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <net/if.h>
#include <linux/if_packet.h>
#include <linux/if_ether.h>
#include <arpa/inet.h>

int main() {
    int raw_sock;
    unsigned char buffer[65536];
    struct sockaddr_ll saddr;
    socklen_t saddr_len = sizeof(saddr);

    // 1. 创建原始套接字
    raw_sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    if (raw_sock < 0) {
        perror("socket() failed");
        exit(EXIT_FAILURE);
    }

    printf("[+] Raw socket created (AF_PACKET, SOCK_RAW, ETH_P_ALL)\n");

    // 2. 循环捕获数据帧
    while (1) {
        int data_size = recvfrom(raw_sock, buffer, sizeof(buffer), 0,
                                 (struct sockaddr*)&saddr, &saddr_len);
        if (data_size < 0) {
            perror("recvfrom() error");
            close(raw_sock);
            exit(EXIT_FAILURE);
        }

        // 3. 解析以太网帧头(14 字节)
        struct ethhdr *eth = (struct ethhdr *)buffer;

        printf("\n[+] Captured frame: %d bytes\n", data_size);
        printf("    Dest MAC  : %02x:%02x:%02x:%02x:%02x:%02x\n",
               eth->h_dest[0], eth->h_dest[1], eth->h_dest[2],
               eth->h_dest[3], eth->h_dest[4], eth->h_dest[5]);
        printf("    Src MAC   : %02x:%02x:%02x:%02x:%02x:%02x\n",
               eth->h_source[0], eth->h_source[1], eth->h_source[2],
               eth->h_source[3], eth->h_source[4], eth->h_source[5]);
        printf("    EtherType : 0x%04x\n", ntohs(eth->h_proto));
    }

    close(raw_sock);
    return 0;
}

权限警告:运行上述程序需要 root 权限或 CAP_NET_RAW 能力。普通用户执行 socket(AF_PACKET, SOCK_RAW, ...) 会返回 EPERM 错误。

4.4 原始套接字数据流与内核交互模型

下图展示了原始套接字在内核网络栈中的位置,以及数据帧从网卡到用户空间的完整路径。

原始套接字数据流与内核交互模型 原始套接字数据流与内核交互模型 用户空间 原始套接字应用 (AF_PACKET, SOCK_RAW) 内核空间 内核协议栈 (TCP/UDP/IP) 原始套接字处理模块 网卡驱动 / 硬件接口 数据帧绕过 内核协议栈 直接到达 原始套接字

模型解读:原始套接字处理模块与内核协议栈并行存在。当网卡驱动收到数据帧时,内核会同时将帧副本投递到协议栈和原始套接字处理模块。因此,原始套接字可以“旁路”TCP/IP 处理,直接获取链路层原始数据。

4.5 常见问题与调试建议

  • 权限不足:使用 sudo 运行程序,或使用 setcap cap_net_raw+ep ./program 赋予能力。
  • 帧过滤:ETH_P_ALL 会捕获所有协议帧,可能导致缓冲区溢出。建议在开发阶段使用 ETH_P_IP 或 ETH_P_ARP 缩小范围。
  • 字节序:以太网头部中的 EtherType 字段使用网络字节序,解析时务必使用 ntohs() 转换。
  • 性能影响:原始套接字会复制所有数据帧到用户空间,高流量环境下可能造成 CPU 负载上升。生产环境建议使用 BPF(Berkeley Packet Filter)进行内核级过滤。

调试技巧:在捕获循环中打印 data_size 和 EtherType,可以快速判断是否成功捕获到目标帧。如果始终收不到数据,检查网卡是否处于混杂模式(可使用 ip link set eth0 promisc on 开启)。