Linux收包之数据L3层是如何流转的

发布时间 2023-12-25 19:47:30作者: 划水的猫

一、环境说明

内核版本:Linux 3.10

内核源码地址:https://elixir.bootlin.com/linux/v3.10/source (包含各个版本内核源码,且网页可全局搜索函数)

网卡:Intel的igb网卡

网卡驱动源码目录:drivers/net/ethernet/intel/igb/

二、L3层概览

 

本章主要介绍收包的流程,在L3层是如何处理的。

类型为ETH_P_IP类型的数据包,被传递到三层,调用ip_rcv函数。

三、ip_rcv函数

netif_receive_skb函数会把指向L3协议指针(skb->nh)设置在L2报头尾端。因此,IP层函数可以安全地将它转换成iphdr结构。
此时,skb->data指向L3报头。
ip_rcv的主要工作就是对封包做健康检查,然后调用Netfilter钩子。

// file: net/ipv4/ip_input.c
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
    const struct iphdr *iph;
    u32 len;

    if (skb->pkt_type == PACKET_OTHERHOST) //丢弃掉不是发往本机的报文,网卡开启混杂模式会收到此类报文
        goto drop;


    IP_UPD_PO_STATS_BH(dev_net(dev), IPSTATS_MIB_IN, skb->len);

    if ((skb = skb_share_check(skb, GFP_ATOMIC)) == NULL) { //检查skb是否为share?是:则克隆报文(克隆报文,内存分配失败的话,该封包会被丢弃)
        IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INDISCARDS);
        goto out;
    }

    if (!pskb_may_pull(skb, sizeof(struct iphdr))) //确保skb->data所指的区域包含的数据区块至少和IP报头一样长,即确保skb还可以容纳标准的报头(即20字节)
        goto inhdr_error;

    iph = ip_hdr(skb); //重新获取IP头(pskb_may_pull可以改变缓冲区结构)

    if (iph->ihl < 5 || iph->version != 4) //ip头长度至少为20字节(ihl>=5,报头的尺寸是4字节的备注),只支持v4
        goto inhdr_error;

    // 这项检查会拖到现在才做,是因为此函数必须先确定基本报头没有被截断,而且从中读取东西前已通过基本健康检查
    if (!pskb_may_pull(skb, iph->ihl*4)) //重复先前做过的检查,只不过这一次使用的是完整的ip报头尺寸。
        goto inhdr_error;

    iph = ip_hdr(skb);

    if (unlikely(ip_fast_csum((u8 *)iph, iph->ihl))) //ip头csum校验
        goto csum_error;

    len = ntohs(iph->tot_len); //取ip分组总长,即ip首部加数据的长度
    if (skb->len < len) { //skb的实际总长度小于ip分组总长,则drop(由于L2协议会填补有效载荷,Ethernet数据帧最小64字节)
        IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INTRUNCATEDPKTS);
        goto drop;
    } else if (len < (iph->ihl*4)) //确保封包的尺寸至少和IP报头的尺寸一样大(IP头不能分段,每个IP片段至少包含一个IP报头)
        goto inhdr_error;

    /* 调整数据包结构与校验和
     * 检查是否有:L2协议填充封包以达到特定的最小长度?
     * 有:把封包剪成正确尺寸,再让L4校验和失效,以免进行接收的NIC计算
     */
    if (pskb_trim_rcsum(skb, len)) { //
        IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INDISCARDS);
        goto drop;
    }

    /* Remove any debris in the socket control block */
    memset(IPCB(skb), 0, sizeof(struct inet_skb_parm)); //清空cb,即inet_skb_parm值

    /* Must drop socket now because of tproxy. */
    skb_orphan(skb);

    //调用netfilter,实现iptables功能,通过后调用ip_rcv_finish函数
    return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
               ip_rcv_finish);

csum_error:
    IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_CSUMERRORS);
inhdr_error:
    IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INHDRERRORS);
drop:
    kfree_skb(skb);
out:
    return NET_RX_DROP;
}

ip_rcv()函数首先是对数据包类型进行检查,如果是PACKET_OTHERHOST类型,则直接丢弃。
skb_share_check()函数检查能否共享数据包结构?能:克隆一个新的数据包结构,函数使用这个新的数据包结构。克隆的数据包可以进行修改。
每一个数据包必须包含一个完整的IP头部,数据块中至少应该包含IP头部,pskb_may_pull()函数检查它是否包含IP头部。
ip_hdr()函数从数据包中取得IP头部,然后对头部进行检查。
ip_fast_csum()函数检查IP头部的校验和。接下来检查数据包的长度是否与头部记录的长度相符,它至少应该等于数据包头部的长度。
接着调用pskb_trim_rcsum()函数为传输层调整数据包与校验和。
ip_rcv()函数最后通过HF_HOOK宏,调用netfilter,实现iptables功能,通过后调用ip_rcv_finish函数。

四、ip_rcv_finish函数