日期:2014-05-16  浏览次数:20653 次

探讨Linux kernel中对序列号超前的ACK包的处理

  在开发的内核模块中遇到这样一个问题:一个数据包有多个请求,每次只让服务器处理一个请求,所以在将请求交到上层的时候需要拆包,只将部分数据交到上层。为了防止客户端重传数据包,要预先给客户端发送一个对完整数据包的确认。这样就会造成一个问题,客户端发送的ACK包的序列号,会比协议栈中期望的序列号大。

  假设完整数据包的起始序列号分别为1883458390、1883458821,上层协议栈拿到的数据包的起始序列号为1883458390、1883458476,这时服务器端sock结构的rcv_nxt应该为1883458476,但是由于我们事先多发送了一个ACK,所以这时客户端的ACK包的序列号为1883458821,而不是1883458476。下面是抓包的部分截图,根据抓包情况来看,这样的ACK包,内核接受了这样的确认包,如图所示(客户端:192.168.9.188;服务器端:192.168.9.191):


OK,现在我们开始来看看内核中是如何处理这样的数据包,为什么会接受这样的ACK包。

  TCP协议的接收函数为tcp_v4_rcv(),该函数SKB进行必要的检查,如数据的长度、校验和初始化等,初始化TCP控制块中的值;接着会调用__inet_lookup_skb()函数在tcp_hashinfo散列表中来查找是否存在对应的传输控制块。如果找到,则调用tcp_v4_do_rcv()来处理(这里我们忽略了Netfilter、IPSec、DMA等细节,这些跟我们探讨的内容无关),基本的代码流程如下图所示:


  我们的ACK包的校验和、首部长度是没有问题,所以它可以轻松的通过tcp_v4_rcv()的检查,接下来看tcp_v4_do_rcv()中处理。tcp_v4_do_rcv()中会首先检查SKB包对应的sock结构的状态,如果是ESTABLISHED状态,则会调用tcp_rcv_established()来处理,代码片段如下:

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
    ......

	if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
		TCP_CHECK_TIMER(sk);
		if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) {
			rsk = sk;
			goto reset;
		}
		TCP_CHECK_TIMER(sk);
		return 0;
	}
	
	......
}
  我们的ACK包是在ESTABLISHED状态下发送的,所以接下来的处理在tcp_rcv_established()中进行。在tcp_rcv_established()中分快速路径和慢速路径,只有在满足一定的条件后才能执行快速路径,其中一个必须判断条件是“TCP_SKB_CB(skb)->seq == tp->rcv_nxt”。由于我们的ACK包的序列号不等于rcv_nxt(即下一个要接收的数据包的序列号),所以我们的SKB包会在慢速路径中执行。tcp_rcv_established()中关键代码如下:

int tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
			struct tcphdr *th, unsigned len)
{
	......
	
	if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags &&
	    TCP_SKB_CB(skb)->seq == tp->rcv_nxt &&
	    !after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt)) {
		......
	}

slow_path:
	if (len < (th->doff << 2) || tcp_checksum_complete_user(sk, skb))
		goto csum_error;

	/*
	 *	Standard slow path.
	 */

	res = tcp_validate_incoming(sk, skb, th, 1);
	if (res <= 0)
		return -res;

step5:
	if (th->ack && tcp_ack(sk, skb, FLAG_SLOWPATH) < 0)
		goto discard;

	tcp_rcv_rtt_measure_ts(sk, skb);

	tcp_urg(sk, skb, th);

	/* step 7: process the segment text */
	tcp_data_queue(sk, skb);

	tcp_data_snd_check(sk);
	tcp_ack_snd_check(sk);
	return 0;

csum_error:
	TCP_INC_STATS_BH(sock_net(sk), TCP_MIB_INERRS);

discard:
	__kfree_skb(skb);
	return 0;
}
  在调用tcp_checksum_complete_user()对SKB包进行校验(我们的ACK包校验和没有问题)后,会调用tcp_validate_incoming()来检查SKB包是否是有效的数据包。tcp_validate_incoming()中主要有以下几种检查:

  ①调用tcp_sequence()检查数据包的序列号是否在接收窗口内

  ②检查RST位是否设置

  ③安全和优先级检查(这个现在已经被忽略)

  ④检查数据包是否是接收窗口内的SYN包

  不难看出,和我们探讨的内容相关的是第二项检查,现在就要看ACK包的序列号是否是在接收窗口内。接收窗口的大小和延迟时间、带宽有关,除非是在极端恶劣的网络情况下,我们的ACK包都会在接收窗口内,所以在这里我们可以认为我们的ACK包能通过tcp_validate_incoming()的检查。
  我们再回到tcp_rcv_established()函数中,在tcp_validate_incoming()检查后,会执行到step5标签处,在这里会调用tcp_ack()来处理我们的ACK包,在调用tcp_ack()处理之后的一些处理可以认为和我们的ACK包无关,不影响最终的结果。也就是说,只要在tcp_ack()中能被内核接受的话,我们的ACK包就是“合法的”数据包,内核就会认为它是一个正确的确认包。所以之后我们只关注tcp_ack()中的处理,不会再回到tcp_rcv_established()中。
  在tcp_ack()中主要的检查是确认的序列号是否在SND.UNA和SND.NXT之间,并没有对在这些检查之后,就开始使用确认的序列号更新内核中的相关结构。我们的ACK包的确认序列号是完全正确的,有问题的是序列号,因此在tcp_ack()中我们的ACK包是可以被接收的。

  通过上面的过程,可以看到我们的ACK包之所以能够被内核接收的关键就是它通过了序列号是否在接收窗口内的检查,而且之后对ACK包的处理中完全没有再对序列号进行检查过,因此我们的有问题的ACK包就在夹缝中通过了内核的检查。

tcp_ack()中具体的处理不多说了,有兴趣的可以看下面的注释:

/* This routine deals with incoming acks, but not outgoing ones. */
/*
 * tcp_ack()用于处理接收到有ACK标志的段,当接到有效的ACK后会更新
 * 发送窗口。
 * @skb: 接收到的ACK段
 * @flag: 标志,取值为FLAG_DATA等
 */
static int tcp_ack(struct sock *sk, struct sk_buff *skb, int flag)
{
	struct in