深入理解Linux网络技术内幕——IPv4 报文的接收(转发与本地传递)

我们知道,报文经过网卡驱动处理后,调用net_receive_skb传递给具体的协议处理函数,对于IPv4报文来说,其协议处理函数就是ip_rcv了,ip_rcv在进行一些健康检查等操作后,会调用ip_rcv_finish来处理报文。这也是IPv4协议对报文接收处理的开始。

我们先看下ip_rcv_finish源代码:

ip_rcv_finish:

//ip数据报文的主要处理程序(ip_rcv仅仅只是对ip数据报做一些健康性检查)
//ip_rcv_finish 其实是进行路由表查询,,决定报文经过IP层处理后,是继续向上传递,还是进行转发,还是丢弃。
//1.决定报文在本地传递或者转发,如果是转发还需要找到出口设备和下一跳节点
//2.分析和处理一些选项
static int ip_rcv_finish(struct sk_buff *skb)
{
	const struct iphdr *iph = ip_hdr(skb);
	struct rtable *rt;

	/*
	 *	Initialise the virtual path cache for the packet. It describe
	 *	how the packet travels inside Linux networking.
	 *  刚开始没有进行路由表查询,所以还没有相应的路由表项:skb_dst(skb) == NULL。
	 *  则在路由表中查找ip_route_input(),关于内核的路由表
	 */
	if (skb_dst(skb) == NULL) {
		int err = ip_route_input(skb, iph->daddr, iph->saddr, iph->tos,
					 skb->dev); //这里面进行了一些初始化操作,比较重要,与ip报文接下来的走向有关
		if (unlikely(err)) {
			if (err == -EHOSTUNREACH)
				IP_INC_STATS_BH(dev_net(skb->dev),
						IPSTATS_MIB_INADDRERRORS);
			else if (err == -ENETUNREACH)
				IP_INC_STATS_BH(dev_net(skb->dev),
						IPSTATS_MIB_INNOROUTES);
			goto drop;
		}
	}

#ifdef CONFIG_NET_CLS_ROUTE
	//更新traffic cotrol(qos层)所使用的统计数据
	if (unlikely(skb_dst(skb)->tclassid)) {
		struct ip_rt_acct *st = per_cpu_ptr(ip_rt_acct, smp_processor_id());
		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))
		goto drop;

	rt = skb_rtable(skb);  /* skb->dst包含路由信息。根据路由类型更新SNMP统计数据 */
	if (rt->rt_type == RTN_MULTICAST) {
		IP_UPD_PO_STATS_BH(dev_net(rt->u.dst.dev), IPSTATS_MIB_INMCAST,
				skb->len);
	} else if (rt->rt_type == RTN_BROADCAST)
		IP_UPD_PO_STATS_BH(dev_net(rt->u.dst.dev), IPSTATS_MIB_INBCAST,
				skb->len);

	/*
     * dst_input实际上会调用skb->dst->input(skb).input函数会根据路由信息设置为合适的
     * 函数指针,如果是递交到本地的则为ip_local_deliver,若是转发则为ip_forward.
     * 暂时仅先考虑ip_local_deliver。
     */
	return dst_input(skb);

drop:
	kfree_skb(skb);
	return NET_RX_DROP;
}

ip_route_input会进行路由表查询,该函数直接或间接决定了报文之后要往何处传递。是进行本地传递还是转发。

我们可以看到如果报文没有被drop掉,那么报文最终会被dst_input(skb)处理。dst_input(skb)实际上执行的是skb->dst->input(skb)。而这里的input函数其实就是由ip_route_input决定的。

对于应该本地传递的报文,input指针会指向ip_local_deliver。对于该转发的报文,input会指向ip_forward。

本地传递

/*
 *  Deliver IP Packets to the higher protocol layers.
 */
int ip_local_deliver(struct sk_buff *skb)
{
    /*
     *  Reassemble IP fragments.
     */

    if (ip_hdr(skb)->frag_off & htons(IP_MF | IP_OFFSET)) {
        if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))
            return 0;
    }   

    return NF_HOOK(PF_INET, NF_INET_LOCAL_IN, skb, skb->dev, NULL,
               ip_local_deliver_finish);
}

我们知道,IPv4要将报文传送给上层协议(本地传递),那它需要对分段的报文进行重组,ip_defrag即完成报文重组。

然后由调用Netfilter决定是否调用ip_local_deliver_finish。

ip_local_deliver_finish

static int ip_local_deliver_finish(struct sk_buff *skb)
{
    struct net *net = dev_net(skb->dev);

    __skb_pull(skb, ip_hdrlen(skb));  /* 跳过IP头部 */

    /* Point into the IP datagram, just past the header. */
    /* 设置传输层头部位置 */
    skb_reset_transport_header(skb);

    rcu_read_lock();
    {
        int protocol = ip_hdr(skb)->protocol; //取出ip头中的协议.
        int hash, raw;
        const struct net_protocol *ipprot;

    resubmit:
        // 若是raw socket发送的,需要做相应的处理,clone数据包
        raw = raw_local_deliver(skb, protocol); //得到raw socket, 如果不是raw socket,则返回0

        hash = protocol & (MAX_INET_PROTOS - 1);  // 计算传输层协议处理结构在inet_protos数组hash表中的位置
        ipprot = rcu_dereference(inet_protos[hash]); // 获取传输层协议处理指针
        if (ipprot != NULL) {
            int ret;

            //主要是ipprot是否有被当前主机注册
            if (!net_eq(net, &init_net) && !ipprot->netns_ok) { // 若获取到了对应传输层的处理结构
                if (net_ratelimit())
                    printk("%s: proto %d isn't netns-ready\n",
                        __func__, protocol);
                kfree_skb(skb);
                goto out;
            }

            //判断ipsec,并进行相关处理.
            if (!ipprot->no_policy) {
                if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
                    kfree_skb(skb);
                    goto out;
                }
                nf_reset(skb);
            }

            //调用handler,进入相应的4层协议的处理.
            ret = ipprot->handler(skb);
            if (ret < 0) {  // 处理数据包失败,再次尝试
                protocol = -ret;
                goto resubmit;
            }
            IP_INC_STATS_BH(net, IPSTATS_MIB_INDELIVERS);// 添加数据包处理统计信息
        } else {// 若没有找到相应传输层的处理函数
          if (!raw) {
                if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
                    IP_INC_STATS_BH(net, IPSTATS_MIB_INUNKNOWNPROTOS);
                    icmp_send(skb, ICMP_DEST_UNREACH,
                          ICMP_PROT_UNREACH, 0);
                }
            } else
                IP_INC_STATS_BH(net, IPSTATS_MIB_INDELIVERS);
            kfree_skb(skb);
        }
    }
 out:
    rcu_read_unlock();

    return 0;
}

转发

报文转发有下面几个步骤完成:

1. 处理IP选项

2. 确定封包可以被转发

3.递减封包头部的TTL字段,如果TTL字段为0,则丢弃该封包

4.根据路径相关MTU,在必要时处理分段

5.把封包传送至外出设备

IPv4协议中,报文的转发从ip_forward开始:

ip_forward

int ip_forward(struct sk_buff *skb)
{
    struct iphdr *iph;  /* Our header */
    struct rtable *rt;  /* Route we use */
    struct ip_options * opt = &(IPCB(skb)->opt);

    if (skb_warn_if_lro(skb))
        goto drop;

    if (!xfrm4_policy_check(NULL, XFRM_POLICY_FWD, skb))
        goto drop;

    if (IPCB(skb)->opt.router_alert && ip_call_ra_chain(skb)) //处理Router_Alert选项(重要)
        return NET_RX_SUCCESS;

    //确定二层目的是发往本机的,这步检查是多余的,因为在二层接收的时候,不是二层地址不是本机的包已经被丢弃
    //对于二层地址为本机的数据帧,skb->pkt_type 赋值为PACKET_HOST
    if (skb->pkt_type != PACKET_HOST)//
        goto drop;

    skb_forward_csum(skb);//这里只是转发封包,因而不关系L4层检验和
  skb_forward_csum(skb);//这里只是转发封包,因而不关系L4层检验和

    /*
     *  According to the RFC, we must first decrease the TTL field. If
     *  that reaches zero, we must reply an ICMP control message telling
     *  that the packet's lifetime expired.
     */
    if (ip_hdr(skb)->ttl <= 1)
        goto too_many_hops;

    if (!xfrm4_route_forward(skb))
        goto drop;

    rt = skb_rtable(skb);
    //如果报头含有strictroute选项,且选项中的下一跳与路由子系统的网关不同
    //则表示选项失败,封包丢弃
    if (opt->is_strictroute && rt->rt_dst != rt->rt_gateway)
        goto sr_failed;

    if (unlikely(skb->len > dst_mtu(&rt->u.dst) && !skb_is_gso(skb) &&
             (ip_hdr(skb)->frag_off & htons(IP_DF))) && !skb->local_df) {
        IP_INC_STATS(dev_net(rt->u.dst.dev), IPSTATS_MIB_FRAGFAILS);
        icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED,
              htonl(dst_mtu(&rt->u.dst)));
        goto drop;
    }

    /* We are about to mangle packet. Copy it! */
    if (skb_cow(skb, LL_RESERVED_SPACE(rt->u.dst.dev)+rt->u.dst.header_len))
        goto drop;
    iph = ip_hdr(skb);

    /* Decrease ttl after skb cow done */
    ip_decrease_ttl(iph);

    /*
     *  We now generate an ICMP HOST REDIRECT giving the route
     *  we calculated.
     */
    if (rt->rt_flags&RTCF_DOREDIRECT && !opt->srr && !skb_sec_path(skb))
        ip_rt_send_redirect(skb);

    skb->priority = rt_tos2priority(iph->tos);

    return NF_HOOK(PF_INET, NF_INET_FORWARD, skb, skb->dev, rt->u.dst.dev,
               ip_forward_finish);

sr_failed:
    /*
     *  Strict routing permits no gatewaying
     */
     icmp_send(skb, ICMP_DEST_UNREACH, ICMP_SR_FAILED, 0);
     goto drop;

too_many_hops:
    /* Tell the sender its packet died... */
    IP_INC_STATS_BH(dev_net(skb_dst(skb)->dev), IPSTATS_MIB_INHDRERRORS);
    icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0);
drop:
    kfree_skb(skb);
    return NET_RX_DROP;
}   

ip_forward检查结束,如果进入ip_forward_finish说明该封包已经可以真正的传给另一个系统了。转发真正工作在ip_forward_finish,中完成。

ip_forward_finish

static int ip_forward_finish(struct sk_buff *skb)
{
    struct ip_options * opt = &(IPCB(skb)->opt);

    IP_INC_STATS_BH(dev_net(skb_dst(skb)->dev), IPSTATS_MIB_OUTFORWDATAGRAMS); 

    //ip_forward已经处理了两个可能的选项,ROute_Alert 和 Strict Source Routing
    //这里还需要处理其他选项(由ip_rcv_finish通过ip_options_compile进行初始化
    if (unlikely(opt->optlen))
        ip_forward_options(skb);       

    return dst_output(skb);
}

最后调用dst_ouput将封包传给设备驱动,转发出去,这里其实封包与我们从传输层传递下来要传给其他系统的封包,开始路径一致了。

/* Output packet to network from transport.  */
static inline int dst_output(struct sk_buff *skb)
{
    return skb_dst(skb)->output(skb);
}   

output是虚拟函数,对于单播封包,会初始化为ip_output,对于多播封包,会初始化为ip_mc_output。这两函数也会处理分段,并在最后调用ip_finish_output.

ip_output

int ip_output(struct sk_buff *skb)
{
    struct net_device *dev = skb_dst(skb)->dev;

    IP_UPD_PO_STATS(dev_net(dev), IPSTATS_MIB_OUT, skb->len);

    skb->dev = dev;
    skb->protocol = htons(ETH_P_IP);

    return NF_HOOK_COND(PF_INET, NF_INET_POST_ROUTING, skb, NULL, dev,
                ip_finish_output,
                !(IPCB(skb)->flags & IPSKB_REROUTED));
}

ip_mc_output

int ip_mc_output(struct sk_buff *skb)
{
    struct sock *sk = skb->sk;
    struct rtable *rt = skb_rtable(skb);
    struct net_device *dev = rt->u.dst.dev;

    /*
     *  If the indicated interface is up and running, send the packet.
     */
    IP_UPD_PO_STATS(dev_net(dev), IPSTATS_MIB_OUT, skb->len);

    skb->dev = dev;
    skb->protocol = htons(ETH_P_IP);

    /*
     *  Multicasts are looped back for other local users
     */

    if (rt->rt_flags&RTCF_MULTICAST) {
        if ((!sk || inet_sk(sk)->mc_loop)
#ifdef CONFIG_IP_MROUTE
        /* Small optimization: do not loopback not local frames,
           which returned after forwarding; they will be  dropped
           by ip_mr_input in any case.
           Note, that local frames are looped back to be delivered
           to local recipients.

           This check is duplicated in ip_mr_input at the moment.
         */
            && ((rt->rt_flags&RTCF_LOCAL) || !(IPCB(skb)->flags&IPSKB_FORWARDED))
#endif
        ) {
            struct sk_buff *newskb = skb_clone(skb, GFP_ATOMIC);
            if (newskb)
                NF_HOOK(PF_INET, NF_INET_POST_ROUTING, newskb,
                    NULL, newskb->dev,
                    ip_dev_loopback_xmit);
        }

        /* Multicasts with ttl 0 must not go beyond the host */

        if (ip_hdr(skb)->ttl == 0) {
            kfree_skb(skb);
            return 0;
        }
    }

    if (rt->rt_flags&RTCF_BROADCAST) {
        struct sk_buff *newskb = skb_clone(skb, GFP_ATOMIC);
        if (newskb)
            NF_HOOK(PF_INET, NF_INET_POST_ROUTING, newskb, NULL,
                newskb->dev, ip_dev_loopback_xmit);
    }

    return NF_HOOK_COND(PF_INET, NF_INET_POST_ROUTING, skb, NULL, skb->dev,
                ip_finish_output,
                !(IPCB(skb)->flags & IPSKB_REROUTED));
}   

ip_finish_output

static int ip_finish_output(struct sk_buff *skb)
{
#if defined(CONFIG_NETFILTER) && defined(CONFIG_XFRM)
    /* Policy lookup after SNAT yielded a new policy */
    if (skb_dst(skb)->xfrm != NULL) {
        IPCB(skb)->flags |= IPSKB_REROUTED;
        return dst_output(skb);
    }
#endif
    if (skb->len > ip_skb_dst_mtu(skb) && !skb_is_gso(skb))
        return ip_fragment(skb, ip_finish_output2);//分段
    else
        return ip_finish_output2(skb);
}

ip_finish_output会和邻居子系统进行衔接。细节看 ipv4 传输的博文。

时间: 2024-10-03 23:10:06

深入理解Linux网络技术内幕——IPv4 报文的接收(转发与本地传递)的相关文章

深入理解Linux网络技术内幕——IPv4 报文的传输发送

报文传输,指的是报文离开本机,发往其他系统的过程. 传输可以由L4层协议发起,也可以由报文转发发起. 在深入理解Linux网络技术内幕--IPv4 报文的接收(转发与本地传递)一文中,我们可以看到,报文转发最后会调用dst_output与邻居子系统进行交互,然后传给设备驱动程序. 这里,我们从L4层协议发起的传输,最后也会经历这一过程(调用dst_output).本文讨论的是L4层协议发起的传输,在IPv4协议处理(IP层)中的一些环节. 大蓝图 我们先看下传输环节的大蓝图,以便对传输这一过程有

深入理解Linux网络技术内幕——IPv4 概念

1.大蓝图 大蓝图展示了IPv4协议与其他子系统之间的联系,保罗设备驱动.Netfilter.L4 层协议等之间的互动. IPv4协议中的报文 我们可以大致看出数据在IPv4协议中的流向, 接收报文 设备驱动处理完硬件介绍到的数据后,IPv4协议的ip_rcv函数(net_receive_skb调用)得到了属于IPv4的报文,接着调用ip_rcv_finish对报文进行分析.判断是该转发还是交付本地上层协议. 如果是本地报文,则传给ip_local_deliver处理,如果是转发,那就交付ip_

深入理解Linux网络技术内幕——IPv4 分段与重组

封包的分段和重组是IP协议最重要的工作之一. IPv4报头中有一个len字段(用于表示报文的总长度,单位:字节)占16bit,因此,封包的最大尺寸定义为64K,(2^16/1024=64). 但是,在实际网络传输中,没有几个网络接口能够传输64K这么大的封包,而是有一个MTU表示其最大传输单元.这样,当要传输的封包大于MTU时,就需要对封包进行分段. 这里需要说明,我们指的MTU,不仅仅是出口设备的MTU,它取决于很多因素,如路由表项所用的MTU.出口设备的MTU等. 我们先不用过多与关注MTU

《深入理解Linux网络技术内幕》阅读笔记 --- 路由

一.Linux内核中路由相关的主要数据结构 struct fib_result:对路由表查找后返回该结构,它的内容并不是简单的包含下一跳信息,而且包含其他特性,例如策略路由所需的更多参数. struct fib_rule:表示由策略路由在路由流量时选择路由表的规则 struct fib_node:一条路由表项.例如,该数据结构用于存储由route add或ip route add命令添加一条路由时生成的信息. struct fn_zone:一个zone表示子网掩码长度相同的一组路由 struct

深入理解Linux网络技术内幕——协议处理函数

网络帧在进入网络层时,需要区分不同的网络协议进行处理,这就需要涉及协议处理函数. 首先我们从驱动接收到一个数据帧,分析数据帧在协议栈中自下而上的传输流程. 设备驱动程序在接收到一个数据帧时,会将其保存在一个sk_buff缓冲区数据结构,并对其进行初始化. struct sk_buff { ...... __be16 protocol:16; ...... } 在这个缓冲区结构体中,有一个protocol字段,用于标识网络层的协议. 我们知道网络帧在设备驱动程序中处理后,设备驱动程序会调用neti

深入理解Linux网络技术内幕——路由子系统的概念与高级路由

本文讨论IPv4的路由子系统.(IPv6对路由的处理不同). 基本概念 路由子系统工作在三层,用来转发入口流量. 路由子系统主要设计 路由器.路由.路由表等概念. 路由器: 配备多个网络接口卡(NIC),并且能利用自身网络信息进行入口流量转发的设备. 路由: 流量转发,决定目的地的过程 路由表:转发信息库,该库中储存路由需要本地接收还是转发的信息, 以及转发流量时所需要的信息.(即,信息库用来判断,要不要转发,如果要转发,向哪里转发). 我们了解,路由器有多个网卡,但是多个NIC的设备不一定就是

深入理解Linux网络技术内幕——设备的注册于初始化(一)

副标题:设备注册相关的基本结构的原理框架 设备注册与删除时间 设备在下列两种情况下进行注册: 1)加载NIC驱动时 2)插入热插拔设备时 这里NIC与热插拔设备有些不同.a.对于非热插拔NIC来说,NIC的注册是伴随着其驱动的发生的,而NIC可以内建到内核,也可以作为模块载入,如果内建入内核,则NIC设备和初始化均发生在引导时,如果NIC作为模块加载,则NIC的注册和驱动初始化均发生在模块加载时.b. 对于热插拔NIC设备来说,其驱动已经加载,因此设备的注册发生在插入设备,内核通知关联驱动时.

深入理解Linux网络技术内幕——帧的接收与传输

帧的接收 NAPI与netif_rx(非NAPI) Linux内核获取网络帧到达通知的方式有两中:中断和轮询.(中断值设备向内核发出中断,轮询指linux内核主动轮询设备) 在早起的linux内核中,网络帧主要以中断的方式通知linux内核帧的到达.这是非NAPI方式. 现在的操作系统中,linux内核使用NAPI方式, 获取帧到达的消息.NAPI混合使用了中断和轮询. netif_rx(非NAPI): 每一个帧接收完毕时,设备向内核发送一个中断.(在低流量负载的情况下,这种方式对比轮询优势明显

深入理解Linux网络技术内幕——网络设备初始化

概述 内核的初始化过程过程中,与网络相关的工作如下所示: 内核引导时执行start_kernel,start_kernel结束之前会调用rest_init,rest_init初始化内核线程init(在Linux3-12中为kernel_init). asmlinkage void __init start_kernel(void) { ... parse_early_param();//间接调用parse_args parse_args(...); //处理内核引导程序(boot loader)