深入理解Linux网络技术内幕 第22章 处理分段

处理分段

  • IP分片
  • ip_fragment
    • 慢速分段
    • 快速分段
  • IP重组
    • IP重组与HASH表
    • IP重组要点
    • ip_defrag函数
    • ip_frag_queue函数
      • L4校验和
      • 垃圾回收

IP分片

IP层如果确定一个IP报文分片:

  • 第一个分片offset=0且MF=1
  • 中间的分片offset>0且MF=1
  • 最后一个分片offset>0且MF=0

旧版本的内核在IP层处理IP分片,发送数据的函数可以接受0-64KB数据,当报文大于PMTU时,就必须把数据分割成多个IP报文。最近的版本是让L4协议协助分片任务,L4协议传递一组和PMTU匹配的缓冲区,IP层只需要为每个缓冲区增加IP头即可。
分片方式

  • 快速分片
  • 慢速分片

IP分片主要任务:

  • 把L3数据分成较小的段,和PMTU匹配。
  • 把每个分片的IP头初始化
  • 计算IP校验和

ip_fragment

前面提到ip_append_data/ip_append_page函数,这两个函数为分片做了基础工作。接下来是ip_push_pengding_frames 会将数据通过dst_output彻底将数据交给L3层,而这个函数指针指向ip_output函数。在ip_finish_output中通过调用ip_fragment完成分片工作。

参数skb包含要传输报文的缓冲区,skb有一个已经初始化的IP报文头,这个报文头被调整后复制到所有的分片内。
参数:output用于传输分片的函数。

static int ip_fragment(struct net *net, struct sock *sk, struct sk_buff *skb,
		       unsigned int mtu,
		       int (*output)(struct net *, struct sock *, struct sk_buff *))

检查输入报文是否设置了DF标志无法分段,如果是需要发送ICMP报文并丢弃该IP报文。

	struct iphdr *iph = ip_hdr(skb);

	if ((iph->frag_off & htons(IP_DF)) == 0)
		return ip_do_fragment(net, sk, skb, output);

	if (unlikely(!skb->ignore_df ||
		     (IPCB(skb)->frag_max_size &&
		      IPCB(skb)->frag_max_size > mtu))) {
     
		IP_INC_STATS(net, IPSTATS_MIB_FRAGFAILS);
		icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED,
			  htonl(mtu));
		kfree_skb(skb);
		return -EMSGSIZE;
	}

	return ip_do_fragment(net, sk, skb, output);

ip_do_fragment函数中实现分片逻辑。当sk_buff参数接收数据已经是被分段的时候(由ip_append_data和ip_push_pending_frames产生),使用快速分段
慢速分片被用于:

  • 被转发的报文
  • 调用dst_output前没有分片的本地流量
  • 对缓冲区健康检查不通过无法使用快速分片。

如果任何分片传输失败,ip_fragment立即返回错误代码,后续的分片无法传输。目的主机的只会接收到IP片段的一部分子集,无法将数据重新组合起来。

慢速分段

慢速分段将IP报文分割成多个分片,分片的尺寸根据接口MTU或者PMTU分片决定。

ptr指向要分片报文的开始处,该值随着分片的工作进行而移动。
not_last_frag如果该分片以后还有数据就为真。

slow_path:
	iph = ip_hdr(skb);

	left = skb->len - hlen;		/* Space per frame */
	ptr = hlen;		/* Where to start from */

	/*
	 *	Fragment the datagram.
	 */

	offset = (ntohs(iph->frag_off) & IP_OFFSET) << 3;
	not_last_frag = iph->frag_off & htons(IP_MF);

	/*
	 *	Keep copying data until we run out.
	 */

只会程序在一个循环中,为每个分片建立一个新缓冲区skb2,len是要分片数据的长度,这个值应该八字节对齐。

	while (left > 0) {
     
		len = left;
		/* IF: it doesn't fit, use 'mtu' - the data space left */
		if (len > mtu)
			len = mtu;
		/* IF: we are not sending up to and including the packet end
		   then align the next start on an eight byte boundary */
		if (len < left)	{
     
			len &= ~7;
		}

		/* Allocate buffer */
		skb2 = alloc_skb(len + hlen + ll_rs, GFP_ATOMIC);
		if (!skb2) {
     
			err = -ENOMEM;
			goto fail;
		}

接下来复制一些skb信息到新申请的skb2中,此处的ll_rs是Link Layer Reserved Space的缩写,即L2层报文头的长度,用于给报文流出2层头信息的内存空间。

/*
		 *	Set up data on packet
		 */

		ip_copy_metadata(skb2, skb);
		skb_reserve(skb2, ll_rs);
		skb_put(skb2, len + hlen);
		skb_reset_network_header(skb2);
		skb2->transport_header = skb2->network_header + hlen;

接下来拷贝IP头和一部分载荷数据到skb2,拷贝载荷数据的时候需要判断源skb数据组织方式,有可能数据已经被分段但是校验不通过。具体实现在skb_copy_bits函数中。

		/*
		 *	Copy the packet header into the new buffer.
		 */

		skb_copy_from_linear_data(skb, skb_network_header(skb2), hlen);

		/*
		 *	Copy a block of the IP datagram.
		 */
		if (skb_copy_bits(skb, ptr, skb_transport_header(skb2), len))
			BUG();
		left -= len;

		/*
		 *	Fill in the new header fields.
		 */

接下来初始化skb2的IP头信息,此处要处理IP头的选项信息,在所有分片中只有第一个分片包含完整的选项,在第一个分片中调用ip_options_fragment,其他的分片在skb_copy_from_linear_data函数中拷贝第一个分片的IP头信息。

	/*
		 *	Fill in the new header fields.
		 */
		iph = ip_hdr(skb2);
		iph->frag_off = htons((offset >> 3));

		if (IPCB(skb)->flags & IPSKB_FRAG_PMTU)
			iph->frag_off |= htons(IP_DF);

		/* ANK: dirty, but effective trick. Upgrade options only if
		 * the segment to be fragmented was THE FIRST (otherwise,
		 * options are already fixed) and make it ONCE
		 * on the initial skb, so that all the following fragments
		 * will inherit fixed options.
		 */
		if (offset == 0)
			ip_options_fragment(skb);

最后设置MF标记,更新IP头的长度字段,计算校验和,最后使用传入的函数指针output将数据传送到接下来的流程。

	/*
		 *	Added AC : If we are fragmenting a fragment that's not the
		 *		   last fragment then keep MF on each bit
		 */
		if (left > 0 || not_last_frag)
			iph->frag_off |= htons(IP_MF);
		ptr += len;
		offset += len;

		/*
		 *	Put this fragment into the sending queue.
		 */
		iph->tot_len = htons(len + hlen);

		ip_send_check(iph);

		err = output(net, sk, skb2);
		if (err)
			goto fail;

		IP_INC_STATS(net, IPSTATS_MIB_FRAGCREATES);

快速分段

当skb_buff中的frag_list指针不为NULL时,就会尝试使用快速分段。但是需要符合以下条件:

  • 每个片段不超过PMTU长度
  • 只有最后一个L3的载荷数据长度不是8的倍数
  • 每个skbuff有足够的数据容纳L2报文头
	if (skb_has_frag_list(skb)) {
     
		struct sk_buff *frag, *frag2;
		unsigned int first_len = skb_pagelen(skb);

		if (first_len - hlen > mtu ||
		    ((first_len - hlen) & 7) ||
		    ip_is_fragment(iph) ||
		    skb_cloned(skb) ||
		    skb_headroom(skb) < ll_rs)
			goto slow_path;

		skb_walk_frags(skb, frag) {
     
			/* Correct geometry. */
			if (frag->len > mtu ||
			    ((frag->len & 7) && frag->next) ||
			    skb_headroom(frag) < hlen + ll_rs)
				goto slow_path_clean;

			/* Partially cloned skb? */
			if (skb_shared(frag))
				goto slow_path_clean;

			BUG_ON(frag->sk);
			if (skb->sk) {
     
				frag->sk = skb->sk;
				frag->destructor = sock_wfree;
			}
			skb->truesize -= frag->truesize;
		}
			

接下来依次处理每个分片,和慢速分片相似。需要做的工作包括

  • 从第一个分片中把IP头拷贝到当前分片
  • 设置该分片的偏移量、校验和、MF标记
  • 从第一个分片拷贝skbuff的一些参数
  • 调用output函数
	/* Everything is OK. Generate! */

		err = 0;
		offset = 0;
		frag = skb_shinfo(skb)->frag_list;
		skb_frag_list_init(skb);
		skb->data_len = first_len - skb_headlen(skb);
		skb->len = first_len;
		iph->tot_len = htons(first_len);
		iph->frag_off = htons(IP_MF);
		ip_send_check(iph);

		for (;;) {
     
			/* Prepare header of the next frame,
			 * before previous one went down. */
			if (frag) {
     
				frag->ip_summed = CHECKSUM_NONE;
				skb_reset_transport_header(frag);
				__skb_push(frag, hlen);
				skb_reset_network_header(frag);
				memcpy(skb_network_header(frag), iph, hlen);
				iph = ip_hdr(frag);
				iph->tot_len = htons(frag->len);
				ip_copy_metadata(frag, skb);
				if (offset == 0)
					ip_options_fragment(frag);
				offset += skb->len - hlen;
				iph->frag_off = htons(offset>>3);
				if (frag->next)
					iph->frag_off |= htons(IP_MF);
				/* Ready, complete checksum */
				ip_send_check(iph);
			}

			err = output(net, sk, skb);

			if (!err)
				IP_INC_STATS(net, IPSTATS_MIB_FRAGCREATES);
			if (err || !frag)
				break;

			skb = frag;
			frag = skb->next;
			skb->next = NULL;
		}

		if (err == 0) {
     
			IP_INC_STATS(net, IPSTATS_MIB_FRAGOKS);
			return 0;
		}

		while (frag) {
     
			skb = frag->next;
			kfree_skb(frag);
			frag = skb;
		}
		IP_INC_STATS(net, IPSTATS_MIB_FRAGFAILS);
		return err;

IP重组

IP到达目的主机时,需要将IP分片重组传递给上层处理,而路由器只是让路由器通过即可。

IP重组与HASH表

接收到IP分片后,会将其组织成一张hash表,每个重组的IP报文有一个struct ipq结构,收到的IP分片以链表的形式组织起来。

/* Describe an entry in the "incomplete datagrams" queue. */
struct ipq {
     
	struct inet_frag_queue q;

	u8		ecn; /* RFC3168 support */
	u16		max_df_size; /* largest frag with DF set seen */
	int             iif;
	unsigned int    rid;
	struct inet_peer *peer;
};

每个片段的偏移量存储sk_buff->cb字段内。在分片重组的上下文中,cb字段包含struct ipfrag_skb_cb结构,该结构中包含有struct inet_skb_parm结构用于存储IP选项和标志。

/* Use skb->cb to track consecutive/adjacent fragments coming at
 * the end of the queue. Nodes in the rb-tree queue will
 * contain "runs" of one or more adjacent fragments.
 *
 * Invariants:
 * - next_frag is NULL at the tail of a "run";
 * - the head of a "run" has the sum of all fragment lengths in frag_run_len.
 */
struct ipfrag_skb_cb {
     
	union {
     
		struct inet_skb_parm	h4;
		struct inet6_skb_parm	h6;
	};
	struct sk_buff		*next_frag;
	int			frag_run_len;
};

struct inet_skb_parm {
     
	int			iif;
	struct ip_options	opt;		/* Compiled IP options		*/
	u16			flags;

#define IPSKB_FORWARDED		BIT(0)
#define IPSKB_XFRM_TUNNEL_SIZE	BIT(1)
#define IPSKB_XFRM_TRANSFORMED	BIT(2)
#define IPSKB_FRAG_COMPLETE	BIT(3)
#define IPSKB_REROUTED		BIT(4)
#define IPSKB_DOREDIRECT	BIT(5)
#define IPSKB_FRAG_PMTU		BIT(6)
#define IPSKB_L3SLAVE		BIT(7)

	u16			frag_max_size;
};

IP重组要点

  • 重组时数据分片要存储在内核中,需要限制内存的使用。
  • 组织不同IP分片的hash表可能会失衡,可以成为攻击的目标,造成HASH速度变慢。
  • IP层为每个分片需要一个定时器,超时后要讲该IP报文所有相关的IP分片都丢掉。
  • IP分片中目的主机可能会收到重复片段。原因IP ID的回绕。

ip_defrag函数

ip_defrag函数任务是查找该分片需要放到哪个IP报文中。ip_find负责查找,如果该分片时IP报文第一个分片,则ip_find会新建立一个struct ipq同时启动一个定时器,定时器到期时进行垃圾清理。
ip_frag_queue负责将该分片放置到重组分片链表的适当位置。

int ip_defrag(struct net *net, struct sk_buff *skb, u32 user)
{
     
	struct net_device *dev = skb->dev ? : skb_dst(skb)->dev;
	int vif = l3mdev_master_ifindex_rcu(dev);
	struct ipq *qp;

	__IP_INC_STATS(net, IPSTATS_MIB_REASMREQDS);
	skb_orphan(skb);

	/* Lookup (or create) queue header */
	qp = ip_find(net, ip_hdr(skb), user, vif);
	if (qp) {
     
		int ret;

		spin_lock(&qp->q.lock);

		ret = ip_frag_queue(qp, skb);

		spin_unlock(&qp->q.lock);
		ipq_put(qp);
		return ret;
	}

	__IP_INC_STATS(net, IPSTATS_MIB_REASMFAILS);
	kfree_skb(skb);
	return -ENOMEM;
}

ip_frag_queue函数

该函数把一个IP分片加入到ipq结构,ipq结构存在一个链表用于组织同一IP报文的所有分片。因为所有分片都是用offset字段组织数据,这意味着只有收到最后一个分片才知道最后的长度。
ip_frag_queue主要任务如下:

  • 根据偏移和长度弄清输入分片处于原报文中的位置
  • 根据MF和offset确定该分片是否是最后一个分片,如果是需要计算分片的长度。
  • 将该分片插入链表中适当的位置,此外不同分片可能会有重叠需要处理这种情况。
  • 更新ipq结构

该函数首先检查片段的有效性,检查INET_FRAG_COMPLETE标记是否设置。防止此函数不是IP封包完全接收时被错误调用。

static int ip_frag_queue(struct ipq *qp, struct sk_buff *skb)
{
     
	struct net *net = container_of(qp->q.net, struct net, ipv4.frags);
	int ihl, end, flags, offset;
	struct sk_buff *prev_tail;
	struct net_device *dev;
	unsigned int fragsize;
	int err = -ENOENT;
	u8 ecn;

	if (qp->q.flags & INET_FRAG_COMPLETE)
		goto err;

	if (!(IPCB(skb)->flags & IPSKB_FRAG_COMPLETE) &&
	    unlikely(ip_frag_too_far(qp)) &&
	    unlikely(err = ip_frag_reinit(qp))) {
     
		ipq_kill(qp);
		goto err;
	}

取出偏移量和长度。我们已经知道了偏移量和长度,可以计算出该分片在原IP报文中的位置。

	ecn = ip4_frag_ecn(ip_hdr(skb)->tos);
	offset = ntohs(ip_hdr(skb)->frag_off);
	flags = offset & ~IP_OFFSET;
	offset &= IP_OFFSET;
	offset <<= 3;		/* offset is in 8-byte chunks */
	ihl = ip_hdrlen(skb);

	/* Determine the position of this fragment. */
	end = offset + skb->len - skb_network_offset(skb) - ihl;
	err = -EINVAL;

确定片段是否是最后一个分片,如果是可以计算出原IP报文的总长度并设置INET_FRAG_LAST_IN标记。如果计算出的值和以前的不吻合就说明这个分片或者前面的分片已经损坏,所有分片被丢弃。
如果不是最后一个分片IP报文的长度需要时8的整数倍,如果不是需要将其截断并重新计算校验和。
如果该片段的结束点大于qp->q.len则更新这个字段。
根据IP协议规定,IP报文头不能被分段,必须有载荷数据,因此如果(end == offset)说明IP报文没有载荷视为损毁。

	/* Is this the final fragment? */
	if ((flags & IP_MF) == 0) {
     
		/* If we already have some bits beyond end
		 * or have different end, the segment is corrupted.
		 */
		if (end < qp->q.len ||
		    ((qp->q.flags & INET_FRAG_LAST_IN) && end != qp->q.len))
			goto discard_qp;
		qp->q.flags |= INET_FRAG_LAST_IN;
		qp->q.len = end;
	} else {
     
		if (end&7) {
     
			end &= ~7;
			if (skb->ip_summed != CHECKSUM_UNNECESSARY)
				skb->ip_summed = CHECKSUM_NONE;
		}
		if (end > qp->q.len) {
     
			/* Some bits beyond end -> corruption. */
			if (qp->q.flags & INET_FRAG_LAST_IN)
				goto discard_qp;
			qp->q.len = end;
		}
	}
	if (end == offset)
		goto discard_qp;

接下来pskb_pull函数将skb中的IP报文头数据删除,pskb_trim_rcsum将缓冲区数据长度设置成有效载荷的长度。

	err = -ENOMEM;
	if (!pskb_pull(skb, skb_network_offset(skb) + ihl))
		goto discard_qp;

	err = pskb_trim_rcsum(skb, end - offset);
	if (err)
		goto discard_qp;

	/* Note : skb->rbnode and skb->dev share the same location. */
	dev = skb->dev;
	/* Makes sure compiler wont do silly aliasing games */
	barrier();

IP分片在链表中按照偏移量有序排列,现在需要找到链表插入位置,这个操作由inet_frag_queue_insert函数完成。
接下来更新一些参数。

	prev_tail = qp->q.fragments_tail;
	err = inet_frag_queue_insert(&qp->q, skb, offset, end);
	if (err)
		goto insert_error;

	if (dev)
		qp->iif = dev->ifindex;

	qp->q.stamp = skb->tstamp;
	qp->q.meat += skb->len;
	qp->ecn |= ecn;
	add_frag_mem_limit(qp->q.net, skb->truesize);
	if (offset == 0)
		qp->q.flags |= INET_FRAG_FIRST_IN;

最后判断整个IP报文的所有分片是不是都已经接收完成,如果是调用ip_frag_reasm函数。

	if (qp->q.flags == (INET_FRAG_FIRST_IN | INET_FRAG_LAST_IN) &&
	    qp->q.meat == qp->q.len) {
     
		unsigned long orefdst = skb->_skb_refdst;

		skb->_skb_refdst = 0UL;
		err = ip_frag_reasm(qp, skb, prev_tail, dev);
		skb->_skb_refdst = orefdst;
		if (err)
			inet_frag_kill(&qp->q);
		return err;

L4校验和

入口设备如果支持L4校验,分片的L4校验和已经计算好。但是校验和在以下条件时会失效:

  • 因为长度不是8的倍数,分片被截断
  • 片段重叠。

垃圾回收

前面提到一个IP重组报文建立的过程中(新的ipq结构),会启动一个内核定时器,此定时器用于丢弃IP重组超时的分片。
这个函数由ip_expire完成。如果所有分片中包含第一个分片,需要传递一个ICMP TIME EXCEEDED信息,因为只有第一个报文包含原来的IP报文头。

/*
 * Oops, a fragment queue timed out.  Kill it and send an ICMP reply.
 */
static void ip_expire(struct timer_list *t)
{
     
	struct inet_frag_queue *frag = from_timer(frag, t, timer);
	const struct iphdr *iph;
	struct sk_buff *head = NULL;
	struct net *net;
	struct ipq *qp;
	int err;

	qp = container_of(frag, struct ipq, q);
	net = container_of(qp->q.net, struct net, ipv4.frags);

	rcu_read_lock();
	spin_lock(&qp->q.lock);

	if (qp->q.flags & INET_FRAG_COMPLETE)
		goto out;

	ipq_kill(qp);
	__IP_INC_STATS(net, IPSTATS_MIB_REASMFAILS);
	__IP_INC_STATS(net, IPSTATS_MIB_REASMTIMEOUT);

	if (!(qp->q.flags & INET_FRAG_FIRST_IN))
		goto out;

	/* sk_buff::dev and sk_buff::rbnode are unionized. So we
	 * pull the head out of the tree in order to be able to
	 * deal with head->dev.
	 */
	head = inet_frag_pull_head(&qp->q);
	if (!head)
		goto out;
	head->dev = dev_get_by_index_rcu(net, qp->iif);
	if (!head->dev)
		goto out;


	/* skb has no dst, perform route lookup again */
	iph = ip_hdr(head);
	err = ip_route_input_noref(head, iph->daddr, iph->saddr,
					   iph->tos, head->dev);
	if (err)
		goto out;

	/* Only an end host needs to send an ICMP
	 * "Fragment Reassembly Timeout" message, per RFC792.
	 */
	if (frag_expire_skip_icmp(qp->q.key.v4.user) &&
	    (skb_rtable(head)->rt_type != RTN_LOCAL))
		goto out;

	spin_unlock(&qp->q.lock);
	icmp_send(head, ICMP_TIME_EXCEEDED, ICMP_EXC_FRAGTIME, 0);
	goto out_rcu_unlock;

out:
	spin_unlock(&qp->q.lock);
out_rcu_unlock:
	rcu_read_unlock();
	if (head)
		kfree_skb(head);
	ipq_put(qp);
}

你可能感兴趣的:(深入理解LINUX网络技术内幕,内核,tcpip,c语言)