Linux 使用 setsockopt ICMP_FILTER 过滤 ICMP 报文详解
2025-03-19 14:00:07
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_WILLPASS
和ICMP_FILTER_WILLBLOCK
可以查询过滤的设置状态。
3. 总结
处理ICMP报文过滤的核心就是 icmp_filter
结构体。 我们设置它的位掩码的各个位, 来允许/阻止特定ICMP类型的接收. 使用 setsockopt
和 ICMP_FILTER
选项就可以实现。记住使用SOL_RAW
作为level参数. 另外,可以适当运用头文件里定义的宏, 使代码看起来更专业, 更容易理解。