返回

Linux 使用 setsockopt ICMP_FILTER 过滤 ICMP 报文详解

Linux

Linux下使用setsockopt ICMP_FILTER 过滤ICMP报文

有时候,我们需要在Linux下使用原始套接字(raw socket)处理ICMP报文。比如,我们自己实现ping程序时,会发送ICMP ECHO REQUEST报文,并接收ICMP ECHO REPLY报文。 碰上网络情况复杂的时候, 可能会收到一些其他类型的 ICMP 报文, 这时候,我们就希望过滤掉我们不需要的报文,只保留我们关心的。 这时可以使用 setsockopt 函数,配合 ICMP_FILTER 选项。

man raw 里面关于 ICMP_FILTER 的, 有点让人摸不着头脑:

ICMP_FILTER

Enable a special filter for raw sockets bound to the IPPROTO_ICMP protocol. The value has a bit set for each ICMP message type which should be filtered out. The default is to filter no ICMP messages.

这段话的意思是:ICMP_FILTER 选项可以为绑定到 IPPROTO_ICMP 协议的原始套接字启用一个特殊的过滤器。 过滤器的值是一个位掩码, 每个位代表一种ICMP消息类型,置位表示要过滤掉该类型的消息。 默认情况下不过滤任何ICMP消息。

关键问题在于,这个位掩码怎么设置? 哪些位对应哪些ICMP类型? 手册里没说清楚,网上也搜不到完整的示例. 通过研究, 我们找到了设置这些位的正确方法。

1. 问题原因:文档缺失 和 不明确的类型定义

问题的核心在于两点:

  • 文档不完整: man raw 手册页没有详细说明 ICMP_FILTER 选项的用法,也没有提供具体的ICMP类型与位掩码的对应关系。
  • 类型不明确: 虽然知道要设置一个位掩码,但是不知道具体用什么类型的变量,以及如何操作这些位。 我们通过查阅内核头文件,发现了 struct icmp_filter 的定义 (位于 linux/icmp.h),但是并不确定它是不是正确用法,怎么用。

2. 解决方案:理解icmp_filter结构体

解决这个问题的关键,是正确理解和使用 struct icmp_filter

2.1 struct icmp_filter 的定义

我们先看看在 <linux/icmp.h> (或者有些系统是 <netinet/ip_icmp.h>)里面,这个结构体长什么样子:

struct icmp_filter {
    __u32       data[32];  /* 32*32 = 1024 bits, 对应所有 ICMP 类型 */
};

它内部有一个 data 数组, 包含了32个 __u32 类型的元素。每个 __u32 类型有 32 位, 加起来, data 总共有 32 * 32 = 1024 位。这些位, 足够覆盖所有可能的ICMP类型 (ICMP类型码范围是 0-255)。

2.2 位掩码与ICMP类型的对应

data 数组中的每一个位,都对应着一个 ICMP 类型。data[0] 的最低位(第0位)对应 ICMP 类型 0 (Echo Reply),第1位对应 ICMP 类型 1 (Unused),依此类推。 data[1]的最低位对应 ICMP类型32, 以此类推。

如果你想过滤掉某种ICMP类型, 就把相应的位设置为1;如果想接收某种ICMP类型,就把相应的位设置为0。

2.3 具体操作:设置过滤器

下面详细展示如何操作这个过滤器:

步骤1:包含头文件

#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip_icmp.h> // 或者 #include <linux/icmp.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

步骤2:创建原始套接字

int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (sockfd < 0) {
    perror("socket");
    exit(EXIT_FAILURE);
}

步骤3:定义并设置 icmp_filter

struct icmp_filter filter;
int i;

// 初始化过滤器:默认全置1, 也就是默认过滤所有 ICMP
for (i=0;i<32;i++)
{
        filter.data[i] = ~0;
}

// 取消对ICMP Echo Reply (类型 0) 的过滤
filter.data[0] &= ~ICMP_FILTER_ECHOREPLY;
// 也可以更直接:
// filter.data[0] &= ~(1 << ICMP_ECHOREPLY);

// 如果你想再接收ICMP Destination Unreachable (类型 3), 那么再加一句
filter.data[0] &= ~((1 << ICMP_DEST_UNREACH));

步骤 4: 使用 setsockopt 应用过滤器

if (setsockopt(sockfd, SOL_RAW, ICMP_FILTER, &filter, sizeof(filter)) < 0) {
    perror("setsockopt");
    exit(EXIT_FAILURE);
}

上面, SOL_RAW 指定了选项的级别是原始套接字级别, 不要写成 IPPROTO_ICMP, IPPROTO_ICMP 也能编译通过,但在一些新版本内核/系统上会有问题.

步骤 5: (可选) 安全提示

使用原始套接字需要 root 权限。请确保你的程序在必要时才以 root 权限运行,以降低安全风险。

完整代码示例

#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip_icmp.h> // 或者 #include <linux/icmp.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    if (sockfd < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    struct icmp_filter filter;

      // 初始化过滤器:默认全置1, 也就是默认过滤所有 ICMP
    int i;
    for (i=0;i<32;i++)
    {
          filter.data[i] = ~0;
    }
    // 取消对ICMP Echo Reply (类型 0) 的过滤
    filter.data[0] &= ~(1 << ICMP_ECHOREPLY);
    // 也可以更直接:
    // filter.data[0] &= ~ICMP_FILTER_ECHOREPLY;

    if (setsockopt(sockfd, SOL_RAW, ICMP_FILTER, &filter, sizeof(filter)) < 0) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    // 现在,sockfd只会接收ICMP Echo Reply报文

    char buffer[1024];
    struct sockaddr_in sender;
    socklen_t sender_len = sizeof(sender);
    ssize_t bytes_received;

    while ((bytes_received = recvfrom(sockfd, buffer, sizeof(buffer), 0,
                                       (struct sockaddr *)&sender, &sender_len)) > 0) {
        printf("Received %zd bytes from %s\n", bytes_received, inet_ntoa(sender.sin_addr));

        // 处理接收到的ICMP Echo Reply报文...
        // 例如,检查报文类型,提取数据等. 注意:此处接收到的是包含 IP 头的完整数据包。
           struct ip *ip_hdr = (struct ip *)buffer;
           struct icmphdr *icmp_hdr = (struct icmphdr *)(buffer + (ip_hdr->ip_hl << 2));

            if (icmp_hdr->type == ICMP_ECHOREPLY) {
                printf("  It's an ICMP Echo Reply!\n");
            }
    }

    if (bytes_received < 0) {
        perror("recvfrom");
    }

    close(sockfd);
    return 0;
}

2.4 进阶:使用ICMP定义的宏

netinet/ip_icmp.h ( 或 linux/icmp.h) 头文件中,除了ICMP类型的定义(例如ICMP_ECHOREPLY), 还有一些用于 ICMP_FILTER的宏定义,可以让设置更简洁:

例如:

ICMP_FILTER_SETBLOCK(type, filterp)
ICMP_FILTER_SETPASS(type, filterp)
ICMP_FILTER_WILLPASS(type, filterp)
ICMP_FILTER_WILLBLOCK(type, filterp)

使用示例

    struct icmp_filter filter;

    // 先默认全阻塞。
      for (int i = 0;i<32;i++)
      {
    filter.data[i] = ~0;
      }

      //允许类型为 ICMP_ECHOREPLY 通过。
    ICMP_FILTER_SETPASS(ICMP_ECHOREPLY, &filter);

      //查看filterp的过滤设置是否会让类型type通过
      if (ICMP_FILTER_WILLPASS(ICMP_ECHOREPLY, &filter))
      {
         puts("filter setting will pass icmp echo reply\n");
      }
       //查看filterp的过滤设置是否会阻止类型type。
       if (!ICMP_FILTER_WILLBLOCK(ICMP_ECHOREPLY, &filter))
      {
        puts("filter setting will NOT block icmp echo reply\n");
      }
    if (setsockopt(sockfd, SOL_RAW, ICMP_FILTER, &filter, sizeof(filter)) < 0) {
          perror("setsockopt fail");
        exit(1);
      }

这些宏定义使代码更加清晰易读。ICMP_FILTER_SETBLOCK 宏可以将指定的ICMP类型设置为过滤(阻止),ICMP_FILTER_SETPASS 宏可以将指定的ICMP类型设置为不过滤(允许),ICMP_FILTER_WILLPASSICMP_FILTER_WILLBLOCK 可以查询过滤的设置状态。

3. 总结

处理ICMP报文过滤的核心就是 icmp_filter 结构体。 我们设置它的位掩码的各个位, 来允许/阻止特定ICMP类型的接收. 使用 setsockoptICMP_FILTER 选项就可以实现。记住使用SOL_RAW 作为level参数. 另外,可以适当运用头文件里定义的宏, 使代码看起来更专业, 更容易理解。