Linux网络编程- 原始套接字(Raw Socket)

基本概念

原始套接字(Raw Socket)提供了一种机制,允许应用程序直接访问底层传输协议,绕过操作系统提供的传输层接口。这种套接字通常用于实现新的协议或对现有协议进行低级别的操作。

以下是对原始套接字的详细介绍:

  1. 定义与用途:

    • 原始套接字是直接基于网络层(如IP)的。当使用原始套接字发送数据时,应用程序负责构建完整的协议头。
    • 它常常被用于构造和发送自定义的IP包,如在ping、traceroute等工具中,它们使用ICMP协议构建消息。
  2. 创建:

    • 创建原始套接字与创建其他类型的套接字相似。例如,创建一个用于IPv4和ICMP的原始套接字:socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
  3. 特权:

    • 由于原始套接字允许直接访问底层协议,并可能被用于伪造数据包,所以它通常需要特殊权限(如root权限)。
  4. 工作方式:

    • 发送: 当发送数据,需要提供完整的传输层头部(如TCP、UDP或ICMP头部)。这给了我们控制头部字段的能力,例如伪造源IP地址。
    • 接收: 当使用原始套接字接收数据时,会得到底层协议的完整头部。
  5. 用途与限制:

    • 用途: 原始套接字经常被用于网络诊断工具(如ping和traceroute)、网络攻击和防御、以及某些类型的网络测试。
    • 限制: 大多数操作系统默认会处理某些协议,这可能会导致原始套接字不能接收到这些协议的数据包。例如,操作系统可能会自动处理ICMP回显请求和回显应答,这意味着原始套接字可能无法看到这些数据包。
  6. 注意事项:

    • 由于原始套接字跳过了常规的协议处理,错误的使用可能导致不可预期的网络行为。
    • 操作系统可能提供了某种形式的保护,以防止滥用原始套接字,例如对其使用进行限制。
  7. 跨平台的差异:

    • 不同的操作系统可能对原始套接字的实现和行为有所不同。例如,Windows和Linux在处理和访问原始套接字时存在细微差别。

总的来说,原始套接字是一个非常强大的工具,但也需要谨慎使用。正确使用它需要对网络协议有深入的理解,而滥用它可能导致网络问题或被视为恶意活动。

链路层上的原始套接字

创建链路层的原始套接字允许我们直接与链路层设备(例如以太网适配器)交互,从而可以发送和接收链路层帧,例如以太网帧。这在某些网络工具和应用中非常有用,例如包捕获工具、桥接和交换应用程序。

以下是如何在Linux中创建链路层的原始套接字:

  1. 包括必要的头文件:

    #include 
    #include 
    #include 
    #include 
    #include   /* 需要以太网协议宏 */
    
  2. 创建套接字:
    使用socket()系统调用创建一个原始套接字,具体地,使用AF_PACKET作为地址族和SOCK_RAW作为套接字类型。

    int sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    if (sockfd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }
    

    注意:htons(ETH_P_ALL)表示该套接字将接收所有类型的以太网帧。

  3. 绑定到具体的网络接口:
    如果想指定从哪个接口接收帧,可以使用bind()函数。首先,需要查找网络接口的索引号。

    char *interface_name = "eth0";  // 例如
    struct sockaddr_ll sa;
    memset(&sa, 0, sizeof(struct sockaddr_ll));
    sa.sll_family = AF_PACKET;
    sa.sll_protocol = htons(ETH_P_ALL);
    sa.sll_ifindex = if_nametoindex(interface_name);
    if (bind(sockfd, (struct sockaddr*)&sa, sizeof(struct sockaddr_ll)) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }
    
  4. 发送和接收帧:
    一旦套接字被创建(并可能被绑定),可以使用标准的sendto()recvfrom()函数发送和接收数据。接收到的数据将包括完整的链路层帧。

  5. 关闭套接字:
    使用close(sockfd);关闭套接字。

需要注意的是,访问链路层通常需要特权,因此上述代码通常需要以root权限运行。

示例

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BUFFER_SIZE 2048

int main() {
    int sockfd, n;
    char buffer[BUFFER_SIZE];
    struct sockaddr_ll sa;
    struct ifreq ifr; // For setting promiscuous mode
    char *interface_name = "eth0";

    sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    if (sockfd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // Enable promiscuous mode
    strncpy(ifr.ifr_name, interface_name, IFNAMSIZ);
    if (ioctl(sockfd, SIOCGIFFLAGS, &ifr) == -1) {
        perror("ioctl SIOCGIFFLAGS");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    ifr.ifr_flags |= IFF_PROMISC;
    if (ioctl(sockfd, SIOCSIFFLAGS, &ifr) == -1) {
        perror("ioctl SIOCSIFFLAGS");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    memset(&sa, 0, sizeof(struct sockaddr_ll));
    sa.sll_family = AF_PACKET;
    sa.sll_protocol = htons(ETH_P_ALL);
    sa.sll_ifindex = if_nametoindex(interface_name);
    if (bind(sockfd, (struct sockaddr*)&sa, sizeof(struct sockaddr_ll)) == -1) {
        perror("bind");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    
    printf("Listening on %s in promiscuous mode...\n", interface_name);
    
    while (1) {
        n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, NULL, NULL);
        if (n == -1) {
            perror("recvfrom");
            break;
        }
        printf("Received a frame of length: %d bytes\n", n);
    }
    
    close(sockfd);
    return 0;
}

这个程序的主要目的是使用原始套接字在Linux环境中捕获网络接口上的数据帧。这种原始套接字的功能允许应用程序直接处理网络层以下的协议,如链路层。下面是对程序的详细分析:

  1. 创建原始套接字:

    • 使用socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))创建一个原始套接字,这允许程序接收所有类型的数据帧。
    • 如果套接字创建失败,程序会打印错误并退出。
  2. 启用混杂模式:

    • 为了捕获在网络接口上的所有流量(不仅仅是发送给该主机的流量),程序启用了混杂模式。
    • 使用ioctl系统调用与SIOCGIFFLAGS命令获取当前接口的标志。
    • 设置IFF_PROMISC标志,以启用混杂模式。
    • 再次使用ioctl系统调用与SIOCSIFFLAGS命令来更新接口的标志。
    • 如果在启用混杂模式的过程中出现任何错误,程序会打印错误信息并退出。
  3. 绑定套接字到特定接口:

    • 为了确保从特定的网络接口(在此例中为eth0)接收数据帧,程序绑定了原始套接字到这个接口。
    • 使用if_nametoindex函数获取接口的索引。
    • 使用bind函数绑定套接字。
    • 如果绑定失败,程序会打印错误信息并退出。
  4. 捕获循环:

    • 程序进入一个无限循环,在该循环中,它使用recvfrom函数来接收数据帧。
    • 对于每个接收到的数据帧,程序打印其长度。
    • 如果在接收数据时出现错误,程序将打印错误信息并跳出循环。
  5. 资源清理:

    • 在程序结束之前,它使用close函数关闭原始套接字以释放资源。

总体来说,这个程序创建并绑定了一个原始套接字,启用了混杂模式,并持续监听指定网络接口上的所有数据帧。这种捕获能力使它非常适合于网络监控和分析任务。


补充

启用混杂模式和绑定到特定接口

启用混杂模式和绑定到特定接口是两个相互独立但在某些用例中都必需的操作。

  1. 启用混杂模式 (Promiscuous Mode):

    • 当一个网络接口处于混杂模式时,它会接收到其上所有传输的数据帧,而不仅仅是那些目标地址与其MAC地址匹配的数据帧。这种模式对于一些特定的应用程序(如网络嗅探器和分析器)是非常有用的,因为它们通常需要捕获所有经过的流量,而不仅仅是发送给本机的流量。
    • 混杂模式是在网卡级别设置的,意味着启用后,接口会将所有流量传递给操作系统。但是,如果有多个网络接口(例如,eth0、eth1等),仅仅启用混杂模式并不会告诉我们应该从哪个接口捕获流量。
  2. 绑定到特定接口:

    • 绑定操作确保原始套接字只从指定的接口(在此例中为eth0)接收数据帧。没有这个绑定,套接字可能会从任何启用了混杂模式的接口接收数据帧。
    • 绑定是在套接字级别设置的,它确保只从特定的接口获取数据,而不是从所有接口。

总结来说,启用混杂模式是为了能够看到所有的流量,而绑定到特定接口是为了确定从哪个接口接收这些流量。两者结合使用,允许我们专门监视特定接口上的所有流量。

struct sockaddr_ll

struct sockaddr_ll 是Linux特有的数据结构,用于定义在数据链路层(Layer 2)上的套接字地址,特别是当使用PF_PACKET协议族时。这个结构提供了原始套接字用来发送和接收数据包所需的所有信息。我们来详细分析它的各个字段:

  1. sll_family: 这是地址族字段。对于这个特定的结构,它总是设置为AF_PACKET

  2. sll_protocol: 这是以网络字节序表示的物理层协议。例如,要接收所有的以太网帧,可以设置为htons(ETH_P_ALL)

  3. sll_ifindex: 这是接口索引,用于标识要绑定的网络接口。通常,可以使用if_nametoindex函数将接口名(如 “eth0”)转换为其索引。

  4. sll_hatype: 这是ARP硬件类型。例如,以太网的硬件类型是ARPHRD_ETHER

  5. sll_pkttype: 描述了数据包的类型,可以是如下类型之一:

    • PACKET_HOST: 目标是本地主机。
    • PACKET_BROADCAST: 这是一个广播数据包。
    • PACKET_MULTICAST: 这是一个多播数据包。
    • PACKET_OTHERHOST: 目标是其他主机,但由于某种原因被本机接收。
    • PACKET_OUTGOING: 由本机生成的输出数据包。
      …等等。
  6. sll_halen: 物理层地址的长度。例如,对于以太网,这个长度是6。

  7. sll_addr: 存储物理层地址(通常是MAC地址)的数组。由于MAC地址的长度为6字节而这个字段为8字节,所以通常后两个字节保持为0。

总之,struct sockaddr_ll结构提供了在数据链路层上发送和接收数据包所需的所有信息,特别是当使用原始套接字和PF_PACKET协议族时。

PF_PACKET 和 AF_PACKET 实际上是同一个值,只是在不同的上下文中使用了不同的名称。在套接字编程中,通常使用 AF_ 前缀来表示地址族,而使用 PF_ 前缀来表示协议族。在 Linux 中,PF_PACKET 和 AF_PACKET 的值是相同的,因此在 socket() 调用中使用哪个取决于个人习惯。

你可能感兴趣的:(Linux,工程化C,linux,网络)