IPv4之接收数据包流程

本文介绍了IPv4数据包从接收到转发的完整流程,包括ip_rcv()的初步校验、ip_rcv_finish()的路由查询、ip_local_delivery()的本地分发以及ip_forward()的转发处理。通过路由查询决定数据包是交给本地高层协议还是进行转发,并详细描述了数据包在Netfilter中的PREROUTING和FORWARD点的角色。
摘要由CSDN通过智能技术生成


IPv4之协议族初始化中有介绍,IPv4协议会向设备接口层注册接收处理函数,使得ETH_P_IP类型的数据包将会交由ip_rcv()函数处理。这篇笔记就从该函数入手,一直到将数据包递交给更高层协议为止,从宏观角度分析下数据包在IP层的传递过程。

主要涉及如下文件:

源代码路径说明
net/ipv4/ip_input.cIP协议输入报文处理过程
net/ipv4/ip_forward.cIP协议转发报文处理过程

接收报文入口: ip_rcv()

我们知道,设备接口层最后会在netif_receive_skb()函数中,根据skb->protocol字段查表,将skb递交给更高层的协议处理,对于IPv4来讲,其注册的接收函数就是ip_rcv():

@skb: 数据包
@dev:数据包的当前输入网络设备(层二可能会使用一些聚合技术)
@pt:数据包的类型
@orig_dev: 接收数据包的原始网络设备
int ip_rcv(struct sk_buff *skb, struct net_device *dev,
	struct packet_type *pt, struct net_device *orig_dev)
{
	struct iphdr *iph;
	u32 len;

	if (dev->nd_net != &init_net)
		goto drop;

	/* When the interface is in promisc. mode, drop all the crap
	 * that it receives, do not try to analyse it.
	 */
    // 在混杂模式下,发往其它主机的一些数据包有可能会到达这里,IPv4并不关注这种包,忽略它们
	if (skb->pkt_type == PACKET_OTHERHOST)
		goto drop;
	IP_INC_STATS_BH(IPSTATS_MIB_INRECEIVES);

	// 因为后面需要修改skb的内容,所以如果skb是被共享的,那么需要克隆一个新的
	if ((skb = skb_share_check(skb, GFP_ATOMIC)) == NULL) {
		IP_INC_STATS_BH(IPSTATS_MIB_INDISCARDS);
		goto out;
	}
	// 确保skb数据区中至少有IP首部长度个字节的数据,如果不满足,该函数会从frags数组中拷贝
	if (!pskb_may_pull(skb, sizeof(struct iphdr)))
		goto inhdr_error;
	// pskb_may_pull()可能会调整内存,所以需要重新计算iph
	iph = ip_hdr(skb);

	/*
	 *	RFC1122: 3.1.2.2 MUST silently discard any IP frame that fails the checksum.
	 *
	 *	Is the datagram acceptable?
	 *
	 *	1.	Length at least the size of an ip header
	 *	2.	Version of 4
	 *	3.	Checksums correctly. [Speed optimisation for later, skip loopback checksums]
	 *	4.	Doesn't have a bogus length
	 */
	// 1&2:检查首部长度和IP协议版本号
	if (iph->ihl < 5 || iph->version != 4)
		goto inhdr_error;
	// 这里之所以又做一遍,是因为IP首部可能还有选项部分,iph->ihl*4是IP报文的真实首部长度
	if (!pskb_may_pull(skb, iph->ihl*4))
		goto inhdr_error;
	iph = ip_hdr(skb);
	// 检查IP首部的校验和,确保IP报头传输没有问题
	if (unlikely(ip_fast_csum((u8 *)iph, iph->ihl)))
		goto inhdr_error;
	
    // 校验IP数据包的总长度
	len = ntohs(iph->tot_len);
	if (skb->len < len) {
		IP_INC_STATS_BH(IPSTATS_MIB_INTRUNCATEDPKTS);
		goto drop;
	} else if (len < (iph->ihl*4))
		goto inhdr_error;

	/* Our transport medium may have padded the buffer out. Now we know it
	 * is IP we can trim to the true length of the frame.
	 * Note this now means skb->len holds ntohs(iph->tot_len).
	 */
    // 如注释所述,层二有可能会在IP数据包上打padding,所这里知道了IP数据包的总长度,
    // 需要对SKB的长度字段进行调整并重新计算L4校验和(因为硬件在校验时是包含了这部分
    // padding的,所以其结果不准确)
	if (pskb_trim_rcsum(skb, len)) {
		IP_INC_STATS_BH(IPSTATS_MIB_INDISCARDS);
		goto drop;
	}

	// 将IP控制块内容全部清零,后面IP层处理过程中会使用该控制块数据结构
	memset(IPCB(skb), 0, sizeof(struct inet_skb_parm));
	// 让数据包通过Netfilter的PREROUTING点,如果通过则将数据包传递给ip_rcv_finish()继续处理
	return NF_HOOK(PF_INET, NF_INET_PRE_ROUTING, skb, dev, NULL, ip_rcv_finish);

inhdr_error:
	IP_INC_STATS_BH(IPSTATS_MIB_INHDRERRORS);
drop:
	kfree_skb(skb);
out:
	return NET_RX_DROP;
}

从上面可以看出,ip_rcv()只是对IP数据包做一些基本的校验(长度检查、检验和等),并没有做任何选项、分段以及路由相关的处理。函数最后让数据包通过Netfilter的PREROUTING点,通过后会调用ip_rcv_finish()继续处理。

路由查询: ip_rcv_finish()

IP报文安全通过netfilter的PREROUTING点后,就会调用ip_rcv_finish()函数。

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 describes
	 *	how the packet travels inside Linux networking.
	 */
    // 如果数据包还没有路由信息(Netfilter可能会查询路由),则通过路由子系统
    // 的ip_route_input()查询路由,进而决定该数据包的去向
	if (skb->dst == NULL) {
		// 路由查询失败,那么会更新统计信息后丢弃数据包
		int err = ip_route_input(skb, iph->daddr, iph->saddr, iph->tos, skb->dev);
		if (unlikely(err)) {
			if (err == -EHOSTUNREACH)
				IP_INC_STATS_BH(IPSTATS_MIB_INADDRERRORS);
			else if (err == -ENETUNREACH)
				IP_INC_STATS_BH(IPSTATS_MIB_INNOROUTES);
			goto drop;
		}
	}
	// 如果该数据包包含IP选项,则解析这些选项,IP选项并不常用,这里不展开
	if (iph->ihl > 5 && ip_rcv_options(skb))
		goto drop;
	// 根据目的路由信息,如果需要,更新多播和广播统计
	rt = (struct rtable*)skb->dst;
	if (rt->rt_type == RTN_MULTICAST)
		IP_INC_STATS_BH(IPSTATS_MIB_INMCASTPKTS);
	else if (rt->rt_type == RTN_BROADCAST)
		IP_INC_STATS_BH(IPSTATS_MIB_INBCASTPKTS);
	// 根据路由结果决定继续上高层协议栈发送,还是转发出去
	return dst_input(skb);
drop:
	kfree_skb(skb);
	return NET_RX_DROP;
}

该函数做的最重要的事情就是路由查找,通过路由查询,决定数据包是继续交由本机的高层协议处理,还是走转发流程,不同的路由是由dst_input()函数决定的:

static inline int dst_input(struct sk_buff *skb)
{
	return skb->dst->input(skb);
}

这里,如果数据是输入本机的,input函数为ip_local_delivery();如果是转发的,input函数为ip_forward()。

数据包输入到本机: ip_local_delivery()

/*
 * 	Deliver IP Packets to the higher protocol layers.
 */
int ip_local_deliver(struct sk_buff *skb)
{
	// 检查该IP报文是否是分片,如果是则用ip_defrag()进行分片重组。
	// 若组装成功则继续处理,否则先进行缓存等待其它分片的到达
	if (ip_hdr(skb)->frag_off & htons(IP_MF | IP_OFFSET)) {
		if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))
			return 0;
	}
	// 让数据包通过Netfilter的LOCAL_IN点, 如果通过则调用ip_local_deliver_finish()继续处理
	return NF_HOOK(PF_INET, NF_INET_LOCAL_IN, skb, skb->dev, NULL, ip_local_deliver_finish);
}

这里我们假设数据包能够通过LOCAL_IN,继续看ip_local_deliver_finish()的处理。

向高层协议分发: ip_local_deliver_finish()

static int ip_local_deliver_finish(struct sk_buff *skb)
{
	// 在skb中将IP首部删掉
	__skb_pull(skb, ip_hdrlen(skb));
    // 设置skb->transport_header指针使其指向skb的data开始位置,这样方便更高层协议处理
	skb_reset_transport_header(skb);

	rcu_read_lock();
	{
    	// 取出IP首部的协议字段,根据该字段寻找对应的上层协议
		int protocol = ip_hdr(skb)->protocol;
		int hash, raw;
		struct net_protocol *ipprot;

	resubmit:
    	// RAW套接字相关,忽略
		raw = raw_local_deliver(skb, protocol);
		// 计算好哈希值
		hash = protocol & (MAX_INET_PROTOS - 1);
        // 从inet_protos数组中寻找上层协议提供的接收处理回调,在协议族初始化时,
        // 所有的上层协议都会将自己的接收处理接口注册到该数组中
		if ((ipprot = rcu_dereference(inet_protos[hash])) != NULL) {
			int ret;
			// IPSec相关的检查,忽略
			if (!ipprot->no_policy) {
				if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
					kfree_skb(skb);
					goto out;
				}
				nf_reset(skb);
			}
            // 调用传输层接口处理,对于TCP是tcp_v4_rcv()
			ret = ipprot->handler(skb);
            // 如果上层的处理返回错误,这里会将错误码作为协议号,重新执行上述流程,
            // 这一般会匹配到ICMP模块进行处理
			if (ret < 0) {
				protocol = -ret;
				goto resubmit;
			}
			IP_INC_STATS_BH(IPSTATS_MIB_INDELIVERS);
		} else {
			if (!raw) {
				if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
					IP_INC_STATS_BH(IPSTATS_MIB_INUNKNOWNPROTOS);
					icmp_send(skb, ICMP_DEST_UNREACH,
						  ICMP_PROT_UNREACH, 0);
				}
			} else
				IP_INC_STATS_BH(IPSTATS_MIB_INDELIVERS);
            // 没有对应的上层协议时,会丢弃该数据包
			kfree_skb(skb);
		}
	}
 out:
	rcu_read_unlock();
	return 0;
}

转发数据包: ip_forward()

ip_rcv_finish()中,经过路由后,如果发现数据包需要被转发,那么会交给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);

	// IPSec相关检查,忽略
	if (!xfrm4_policy_check(NULL, XFRM_POLICY_FWD, skb))
		goto drop;
	// 如果有路由告警信息,处理成功后直接返回,不再转发这种数据包
	if (IPCB(skb)->opt.router_alert && ip_call_ra_chain(skb))
		return NET_RX_SUCCESS;

	// 确保该数据包确实是让自己转发的
	if (skb->pkt_type != PACKET_HOST)
		goto drop;
	// 转发会修改IP的首部字段,所以需要把检验和设置为CHECKSUM_NONE
	skb_forward_csum(skb);

	/*
	 *	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.
	 */
    // 如果TTL已经减为1,那么向发送段回复生命周期太短的ICMP报文
	if (ip_hdr(skb)->ttl <= 1)
		goto too_many_hops;
	// IPSec相关,忽略
	if (!xfrm4_route_forward(skb))
		goto drop;

	// 严格源路由选项检查
	rt = (struct rtable*)skb->dst;
	if (opt->is_strictroute && rt->rt_dst != rt->rt_gateway)
		goto sr_failed;
	// IP分片相关处理
	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(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);

    // 递减TTL并更新校验和
	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->sp)
		ip_rt_send_redirect(skb);

	// 根据TOS字段转换出优先级
	skb->priority = rt_tos2priority(iph->tos);

	// 让数据包通过Netfilter的FORWARD点,如果通过调用ip_forward_finish()完成后续转发过程处理
	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(IPSTATS_MIB_INHDRERRORS);
	icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0);
drop:
	kfree_skb(skb);
	return NET_RX_DROP;
}

ip_forward_finish()

转发的数据包通过Netfilter的FORWARD点后,会由ip_forward_finish()继续处理数据包。

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

	IP_INC_STATS_BH(IPSTATS_MIB_OUTFORWDATAGRAMS);
	// 处理转发选项
	if (unlikely(opt->optlen))
		ip_forward_options(skb);
	// 直接调用路由输出,指向的应该是ip_output()或者ip_mc_output()
	return dst_output(skb);
}

到dst_output(),后续流程和本机的正常发包一样了。

总结

从上面的分析中,可以大致总结出数据包的接收流程如下:

  1. 设备接口层处理完数据包后,调用ip_rcv()将数据包交由IP层继续处理;
  2. IP层首先做些简单的校验后,让数据包过netfilter的PREROUTING点;
  3. PREROUTING点通过后,进行路由查询,决定是将数据包递交给本机,还是转发;
  4. 对于递交给本机的数据包,让其继续过Netfilter的LOCAL_IN点。通过后会根据IP首部的协议字段,查找高层协议处理函数,然后调用该函数,进而将数据包交给高层协议处理;
  5. 对于需要转发的数据包,根据转发的需要,修改IP首部内容,然后过其过Netfilter的FORWARD点,最后走和本机发送数据包一样的流程将数据包转发出去。

这篇笔记重点在于宏观上把握数据包在IP层的接收流程,至于其中的细节:如路由查询、防火墙等内容可以先忽略,在对应的笔记中会继续详细展开。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值