Return

从 ICMP 差错报文到 sk_buff 字段

Table of Contents

最近在复习计算机网络关于 ICMP 协议的知识点时,看到一条经典的铁律:

“为了防止广播风暴,针对目的地址是广播或多播的 IP 数据包,接收方产生差错时,不会发送 ICMP 差错报文。”

这条规则本身很好理解,但我产生了一个疑问。

问题的起因

ICMP 差错报文是由 网络层(Layer 3) 发出的。但是按照 OSI 或 TCP/IP 的分层封装原则,数据包从物理层向上传递时,数据链路层(Layer 2) 的帧头(包含 MAC 地址)会被剥离,只把剩下的净荷(IP 包)交给网络层。

那么问题来了: “既然 MAC 头都已经去掉了,接收站点的网络层是怎么知道这段数据在链路层是广播发过来的?如果不知道,它又是依据什么来决定‘不发 ICMP’的?”

听起来像是网络层“越级”去偷看了链路层的信息。

分析与解答

经过查阅资料,发现了之前的认知盲点:

分层间的数据传递没有那么简单。

实际上,操作系统内核在各层之间传递数据时,传的不只是**“数据本身”** ,还有一个包含各种**“元数据(Metadata)”**的结构体。

即使 MAC 头被物理剥离了,其源地址这个关键属性,也会被打成一个标签 ,贴在数据包上,一直传给网络层。

Linux 内核的 sk_buff

以 Linux 内核为例,网络数据包在内核中通常封装在一个叫做 sk_buff(socket buffer)的结构体对象中。

把这个结构体想象成一个文件夹

  • 文件内容:真正的 IP 数据包(去掉 MAC 头后)。
  • 文件夹封面(属性):记录了大量元数据,包括数据包的来源、目的地、协议类型、接收接口等信息。

当一个广播帧到达网卡时,内核里发生了这样的“接力”:

  1. 网卡驱动层(Layer 2)接收: 驱动程序读取数据帧,此时它能看到完整的 MAC 帧头,发现目的 MAC 是 FF:FF:FF:FF:FF:FF(广播)。

  2. 打标签(关键步骤): 驱动程序在把数据包向上传递之前,做了两件事:

    • 移动指针:把指向数据开头的指针向后移,跳过 MAC 头(相当于逻辑上去除 MAC 头)。

    • 填写元数据:在 sk_buff 结构体的 pkt_type 字段里写上标记 PACKET_BROADCAST

  3. 网络层(Layer 3)处理: IP 协议栈收到 sk_buff。虽然指针指向的是 IP 头,但它可以直接读取结构体上的 pkt_type 字段。

  4. 伪代码复现

    // IP层处理逻辑示意
    void ip_send_icmp_error(struct sk_buff *skb) {
    
        // ...其他检查...
    
        // 【重点】检查链路层属性
        // 检查随包传上来的“元数据标签”
        if (skb->pkt_type == PACKET_BROADCAST || skb->pkt_type == PACKET_MULTICAST) {
            // 发现这是一个链路层广播包,根据规定,不回发ICMP,直接丢弃
            return; 
        }
    
        // 只有通过所有检查,才发送ICMP
        send_icmp(skb);
    }