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

一、环境说明

内核版本:Linux 3.10

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

网卡:Intel的igb网卡

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

二、L3层概览

 

本章主要介绍收包的流程,在L3层是如何处理的,但是省略了路由查询以及IP封包分段/重组的细节分析(后续单独成章)。

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

三、IP数据包健康检查: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数据包:ip_rcv_finish函数

ip_rcv_finish()函数的主要工作:
明确数据包是本地接收还是转发,如果是转发就需要进一步明确转发网络设备和跳转出口;
解析和处理部分IP选项(转发场景的一些IP选项不在这处理)

// file: net/ipv4/ip_input.c
static int ip_rcv_finish(struct sk_buff *skb)
{
    const struct iphdr *iph = ip_hdr(skb); //获取数据包的IP头部
    struct rtable *rt;

    if (sysctl_ip_early_demux && !skb_dst(skb)) {
        const struct net_protocol *ipprot;
        int protocol = iph->protocol; //得到传输层协议
        /* 找到early_demux函数,如是tcp协议就调用tcp_v4_early_demux */
        ipprot = rcu_dereference(inet_protos[protocol]);
        if (ipprot && ipprot->early_demux) { //对于socket报文,可以通过socket快速获取路由表
            ipprot->early_demux(skb); //调用该函数,将路由信息缓存到_skb->refdst
            /* must reload iph, skb->head might have changed */
            iph = ip_hdr(skb); //重新获取IP头部
        }
    }

    /*
     * 为数据包初始化虚拟路径缓存,它描述了数据包是如何在linux网络中传播的;
     * 通常从外界接收的数据包,skb->dst不会包含路由信息;
     * skb->dst该数据域包含了如何到达目的地址的路由信息。如果该数据域是NULL,就通过路由子系统函数ip_route_input_noref路由,ip_route_input_noref的输入参数有源IP地址、目的IP地址、服务类型、接受数据包的网络设备,根据这5个参数决策路由;
     */
    if (!skb_dst(skb)) {
        int err = ip_route_input_noref(skb, iph->daddr, iph->saddr, iph->tos, skb->dev);
        if (unlikely(err)) {
            if (err == -EXDEV)
                NET_INC_STATS_BH(dev_net(skb->dev), LINUX_MIB_IPRPFILTER);
            goto drop;
        }
    }

/*
 * 更新一些Traffic Control(QOS层)所用的统计数据
 */
#ifdef CONFIG_IP_ROUTE_CLASSID
    if (unlikely(skb_dst(skb)->tclassid)) {
        struct ip_rt_acct *st = this_cpu_ptr(ip_rt_acct);
        u32 idx = skb_dst(skb)->tclassid;
        st[idx&0xFF].o_packets++;
        st[idx&0xFF].o_bytes += skb->len;
        st[(idx>>16)&0xFF].i_packets++;
        st[(idx>>16)&0xFF].i_bytes += skb->len;
    }
#endif

    if (iph->ihl > 5 && ip_rcv_options(skb)) //当IP报头的长度大于20字节(5*32位),表示有一些选项要处理
        goto drop;

    rt = skb_rtable(skb); //取得路由表指针
    if (rt->rt_type == RTN_MULTICAST) { //检查路由类型
        IP_UPD_PO_STATS_BH(dev_net(rt->dst.dev), IPSTATS_MIB_INMCAST, skb->len);
    } else if (rt->rt_type == RTN_BROADCAST)
        IP_UPD_PO_STATS_BH(dev_net(rt->dst.dev), IPSTATS_MIB_INBCAST, skb->len);

    return dst_input(skb); //调用路由表的输入处理函数(skb->dst->input会设置成ip_local_deliver或ip_forward)

drop:
    kfree_skb(skb);
    return NET_RX_DROP;
}

 IP选项处理:

// file: net/ipv4/ip_input.c
static inline bool ip_rcv_options(struct sk_buff *skb)
{
    struct ip_options *opt;
    const struct iphdr *iph;
    struct net_device *dev = skb->dev; //取得网络设备

    /* It looks as overkill, because not all
       IP options require packet mangling.
       But it is the easiest for now, especially taking
       into account that combination of IP options
       and running sniffer is extremely rare condition.
                          --ANK (980813)
    */
    if (skb_cow(skb, skb_headroom(skb))) { //对数据包进行克隆,使用克隆数据包
        IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INDISCARDS);
        goto drop;
    }

    iph = ip_hdr(skb); //重新获取IP头部
    opt = &(IPCB(skb)->opt); //获取IP选项结构指针
    opt->optlen = iph->ihl*4 - sizeof(struct iphdr); //计算IP选项的长度

    if (ip_options_compile(dev_net(dev), opt, skb)) { //分析IP选项
        IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INHDRERRORS);
        goto drop;
    }

    if (unlikely(opt->srr)) { //如果指定了源路由
        struct in_device *in_dev = __in_dev_get_rcu(dev); //取得配置结构

        if (in_dev) {
            if (!IN_DEV_SOURCE_ROUTE(in_dev)) { //如果不支持源路由
                if (IN_DEV_LOG_MARTIANS(in_dev)) //检查是否允许打印信息
                    net_info_ratelimited("source route option %pI4 -> %pI4\n", &iph->saddr, &iph->daddr);
                goto drop;
            }
        }

        if (ip_options_rcv_srr(skb)) //处理源路由,检查或者创建跳转地址的路由表
            goto drop;
    }

    return false;
drop:
    return true;
}

ip_options_compile()函数只会检查那些是否正确,然后将那些选项存储在skb->cb所指的私有数据域字段的ip_option结构内。
当设备允许IP源路由时,程序就会调用ip_options_rcv_srr来设定skb->dst,然后决定要如何使用封包,也就是说,要决定使用哪个设备把该封包转发至来源地路由列表中的下一个跳点。

// file: net/ipv4/ip_options.c
int ip_options_rcv_srr(struct sk_buff *skb)
{
    struct ip_options *opt = &(IPCB(skb)->opt); //获取IP选项结构指针
    int srrspace, srrptr;
    __be32 nexthop;
    struct iphdr *iph = ip_hdr(skb); //获取IP头部
    unsigned char *optptr = skb_network_header(skb) + opt->srr; //指向源路由
    struct rtable *rt = skb_rtable(skb); //获取路由表(前面在本地查找或者创建的)
    struct rtable *rt2;
    unsigned long orefdst;
    int err;

    if (!rt)
        return 0;

    if (skb->pkt_type != PACKET_HOST) //如果数据包是发给本机的就返回
        return -EINVAL;
    if (rt->rt_type == RTN_UNICAST) { //如果路由表是单播,即点对点
        if (!opt->is_strictroute) //并且不是严格路由就返回
            return 0;
        icmp_send(skb, ICMP_PARAMETERPROB, 0, htonl(16<<24)); //发回ICMP信息
        return -EINVAL;
    }
    if (rt->rt_type != RTN_LOCAL) //如果路由表不是本地的也返回
        return -EINVAL;

    for (srrptr=optptr[2], srrspace = optptr[1]; srrptr <= srrspace; srrptr += 4) {
        if (srrptr + 3 > srrspace) { //检查源路由
            icmp_send(skb, ICMP_PARAMETERPROB, 0, htonl((opt->srr+2)<<24));
            return -EINVAL;
        }
        memcpy(&nexthop, &optptr[srrptr-1], 4); //复制第一个跳转地址

        orefdst = skb->_skb_refdst;
        skb_dst_set(skb, NULL); //置空数据包的路由表指针
        err = ip_route_input(skb, nexthop, iph->saddr, iph->tos, skb->dev); //查找跳转地址的路由表,没有就创建
        rt2 = skb_rtable(skb); //获取跳转地址的路由表
        if (err || (rt2->rt_type != RTN_UNICAST && rt2->rt_type != RTN_LOCAL)) { //如果它不是单播类型并且不是本地路由
            skb_dst_drop(skb); //释放跳转地址的路由
            skb->_skb_refdst = orefdst; //仍旧使用原来的路由表
            return -EINVAL;
        }
        refdst_drop(orefdst); //到达这里,使用跳转地址的路由表,因此释放原来的路由表
        if (rt2->rt_type != RTN_LOCAL) //跳转地址的路由表不在本地,就停止检查
            break;
        /* Superfast 8) loopback forward */
        iph->daddr = nexthop; //记录跳转地址作为目标地址
        opt->is_changed = 1; //设置IP选项改变标志
    }
    if (srrptr <= srrspace) { //已经处理了全部跳转地址
        opt->srr_is_hit = 1; //设置源路由命中标志
        opt->nexthop = nexthop;
        opt->is_changed = 1; //设置IP选项改变标志
    }
    return 0;
}

该函数将浏览源路由的全部跳转地址,逐个检查路由表,没有路由表就创建新的路由表(通过ip_route_input函数实现)。
对于本地地址,它会创建本地路由表设置输入处理函数为ip_local_deliver();
对于转发地址,它会创建转发路由表设置输入处理函数为ip_forward();

dst_input()函数调用ip_local_deliver()继续处理输入数据包。

五、本地传递:ip_local_deliver()函数

// file: net/ipv4/ip_input.c
int ip_local_deliver(struct sk_buff *skb)
{
    /*
     *    Reassemble IP fragments.
     */

    if (ip_is_fragment(ip_hdr(skb))) { //如果是分段数据包
        if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER)) //重组分段数据包
            return 0;
    }

    return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL,
               ip_local_deliver_finish); //调用ip_local_deliver_finish传递给TCP层处理
}

先是对数据包IP头部进行检查,查看是否为分段数据包,如果是就调用ip_defrag()函数将分段数据包插入分段队列;
如果接收的是最后一个分段数据包,则重组数据包。
对本地传递而言,原有IP封包一定要始终重组以整体传送才行,因为较高的L4层应该幸福到对IP层的分段需求毫无所知。

六、L3到L4的传递:ip_local_deliver_finish()函数

它的主要工作:根据输入IP封包报头的协议字段找出正确的协议处理函数,然后把该封包交给该处理函数;
同时它还必须处理Raw IP和安全策略的检查。

// file: net/ipv4/ip_input.c
static int ip_local_deliver_finish(struct sk_buff *skb)
{
    ...... //处理Raw IP
    ipprot = rcu_dereference(inet_protos[protocol]);
    ret = ipprot->handler(skb);
    ......
}

至此,skb包就被传递到L4层。

posted @ 2023-12-25 19:44  划水的猫  阅读(127)  评论(0编辑  收藏  举报