linux内核校验tcp包,Linux内核中TCP SACK处理流程分析

SACK介绍:

SACK:Selective Acknowledgment (SACK)(参看),这种方式需要在TCP头里加一个SACK的东西,ACK还是Fast

Retransmit的ACK,SACK则是汇报收到的数据碎版。参看下图:

linux内核校验tcp包,Linux内核中TCP SACK处理流程分析_第1张图片

即使包2(假设从0到9的序列)是在传送过程中惟一丢失的包,接收方也只能对包1发出一个普通的ACK,因为这是连续接收到的包中的最后一个。另一方面,SACK接收方可以发出包1的ACK和包3到包9的SACK选项。这种附加信息可以帮助发送方确定丢失的包最少,只需重新传送很少的数据。如果没有这种附加信息,它需要重新传送大量的数据,这样会降低传送速率,从而适应高丢包率的网络。

在高延迟的连接中,SACK对于有效利用所有可用带宽尤其重要。高延迟会导致在任何给定时刻都有大量正在传送的包在等待应答。在Linux中,除非得到应答或不再需要,这些包将一直存放在重传队列中。这些包按照序列编号排队,但不存在任何形式的索引。当需要处理一个收到的SACK选项时,TCP协议栈必须在重传队列中找到应用了SACK的包。重传队列越长,找到所需的数据就越困难。

每个包中的最多包含4个SACK选项。

SACK会消耗发送方的资源,假设,如果一个攻击者给数据发送方发一堆SACK的选项,这会导致发送方开始要重传甚至遍历已经发出的数据,这会消耗很多发送端的资源。

在正常的网络传输中,在TCP报文的选项中没有SACK,当发送方和接收方都支持SACK选项时,当在传输过程中有数据包丢失,重传发送时,就可以在TCP报文选项中包含SACK选项,用于表明我接收到的数据包,用来提示发送端,接收端没有接收到的数据包。

(1)  tcp_sack参数,

该参数标识是否启用选择性确认SACKS选项。默认值为1(true)。TCP

SACK(Selective

Acknowledgment)

这个可以在/proc/sys/net/ipv4#目录下的tcp_sack文件中查看参数

cat tcp_sack

1

可见Linux内核是默认打开该选项信息的。使用的Linux内核版本为: uname -a

Linux 2.6.32-21-generic-pae #32-Ubuntu SMP Fri Apr 16 09:39:35

UTC 2010 i686 GNU/Linux

SACK可以用来查找特定丢失的段,因此有助于快速恢复状态,同时,启用SACK,接收方可以用选择地应答乱序接收到的段,可帮助发送方确定丢失的段,进而发送方只需要发送丢失的段,以提高性能。对于广域网通信来说应该启用该选项,但是这会增加CPU负荷。

这篇文章主要是分析内核中对该选项信息的处理。该选项信息的宏定义在nf_conntrack_tcp.h文件中:

/* SACK is permitted by the sender */

#define IP_CT_TCP_FLAG_SACK_PERM       0x02

在建立tcp的会话时,会设置该值,在tcp_new函数中。

linux内核校验tcp包,Linux内核中TCP SACK处理流程分析_第2张图片

在tcp_new()函数中有下面的赋值:

static bool tcp_new(struct nf_conn *ct, const struct sk_buff *skb,

unsigned int dataoff)

{

enum tcp_conntrack new_state;

const struct tcphdr *th;

struct tcphdr _tcph;

const struct ip_ct_tcp_state *sender = &ct->proto.tcp.seen[0];

const struct ip_ct_tcp_state *receiver = &ct->proto.tcp.seen[1];

th = skb_header_pointer(skb, dataoff, sizeof(_tcph), &_tcph);

BUG_ON(th == NULL);

/* Don't need lock here: this conntrack not in circulation yet */

new_state

= tcp_conntracks[0][get_conntrack_index(th)]

[TCP_CONNTRACK_NONE];

/* Invalid: delete conntrack */

if (new_state >= TCP_CONNTRACK_MAX) {

pr_debug("nf_ct_tcp: invalid new deleting.\n");

return false;

}

if (new_state == TCP_CONNTRACK_SYN_SENT) {

/* SYN packet */

ct->proto.tcp.seen[0].td_end =

segment_seq_plus_len(ntohl(th->seq), skb->len,

dataoff, th);

ct->proto.tcp.seen[0].td_maxwin = ntohs(th->window);

if (ct->proto.tcp.seen[0].td_maxwin == 0)

ct->proto.tcp.seen[0].td_maxwin = 1;

ct->proto.tcp.seen[0].td_maxend =

ct->proto.tcp.seen[0].td_end;

tcp_options(skb, dataoff, th, &ct->proto.tcp.seen[0]);

ct->proto.tcp.seen[1].flags = 0;

} else if (nf_ct_tcp_loose == 0) {

/* Don't try to pick up connections. */

return false;

} else {

/*

* We are in the middle of a connection,

* its history is lost for us.

* Let's try to use the data from the packet.

*/

ct->proto.tcp.seen[0].td_end =

segment_seq_plus_len(ntohl(th->seq), skb->len,

dataoff, th);

ct->proto.tcp.seen[0].td_maxwin = ntohs(th->window);

if (ct->proto.tcp.seen[0].td_maxwin == 0)

ct->proto.tcp.seen[0].td_maxwin = 1;

ct->proto.tcp.seen[0].td_maxend =

ct->proto.tcp.seen[0].td_end +

ct->proto.tcp.seen[0].td_maxwin;

ct->proto.tcp.seen[0].td_scale = 0;

/* We assume SACK and liberal window checking to handle

* window scaling */

ct->proto.tcp.seen[0].flags =

ct->proto.tcp.seen[1].flags = IP_CT_TCP_FLAG_SACK_PERM |

IP_CT_TCP_FLAG_BE_LIBERAL;

}

在tcp_in_window函数中有下面的配置

static bool tcp_in_window(const struct nf_conn *ct,

struct ip_ct_tcp *state,

enum ip_conntrack_dir dir,

unsigned int index,

const struct sk_buff *skb,

unsigned int dataoff,

const struct tcphdr *tcph,

u_int8_t pf)

{

struct net *net = nf_ct_net(ct);

struct ip_ct_tcp_state *sender = &state->seen[dir];

struct ip_ct_tcp_state *receiver = &state->seen[!dir];

const struct nf_conntrack_tuple *tuple = &ct->tuplehash[dir].tuple;

__u32 seq, ack, sack, end, win, swin;

s16 receiver_offset;

bool res;

/*

* Get the required data from the packet.

*/

seq = ntohl(tcph->seq);

ack = sack = ntohl(tcph->ack_seq);

win = ntohs(tcph->window);

end = segment_seq_plus_len(seq, skb->len, dataoff, tcph);

if (receiver->flags & IP_CT_TCP_FLAG_SACK_PERM)//根据是否设置了该标志位进行相应的处理

tcp_sack(skb, dataoff, tcph, &sack);

……………..

}

下面RFC2018中关于TCP  SACK Option的结构描述。

linux内核校验tcp包,Linux内核中TCP SACK处理流程分析_第3张图片

下面是通过Wireshark抓包工具,抓取到的含有TCP SACK的报文格式。

linux内核校验tcp包,Linux内核中TCP SACK处理流程分析_第4张图片

通过上面的报文格式可以看到,其表现形式符合RFC描述的结构。我们看到上面的报文中含有NOP,该选项表示的是填充值,为了32bit对齐。

我们看一下Linux内核怎么处理有该选项的数据包的,下面是Linux2.6.32内核版中处理SACK的函数tcp_sack.

点击(此处)折叠或打开

static void tcp_sack(const struct sk_buff *skb, unsigned int dataoff,

const struct tcphdr *tcph, __u32 *sack)

{

unsigned char buff[(15 * 4) - sizeof(struct tcphdr)];

const unsigned char *ptr;

int length = (tcph->doff*4) - sizeof(struct tcphdr);

__u32 tmp;

if (!length)

return;

ptr = skb_header_pointer(skb, dataoff + sizeof(struct tcphdr),

length, buff);

BUG_ON(ptr == NULL);

/* Fast path for timestamp-only option */

if (length == TCPOLEN_TSTAMP_ALIGNED*4

&& *(__be32 *)ptr == htonl((TCPOPT_NOP << 24)

| (TCPOPT_NOP << 16)

| (TCPOPT_TIMESTAMP << 8)

| TCPOLEN_TIMESTAMP))

return;

while (length > 0) {

int opcode = *ptr++;//获取选项码,

int opsize, i;

switch (opcode) {

case TCPOPT_EOL:

return;

case TCPOPT_NOP:    /* Ref: RFC 793 section 3.1 */

length--;

continue;

default:

opsize = *ptr++;

//下面的代码主要判断选项长度的合法性

if (opsize < 2) /* "silly options" */

return;

if (opsize > length)

break;    /* don't parse partial options */

/*下面的代码主要判断,如果选项的code为TCPOPT_SACK表示含有SACK选项信息,并且包含的选项大小大于10,因为 Left Edge of Block 和TCPOLEN_SACK_PERBLOCK各占4个字节+长度1个字节+Code 一个字节,选项中可能含有多个 edge of block,通过for循环确定第一个SACK块。

#define TCPOLEN_SACK_BASE        2

#define TCPOLEN_SACK_PERBLOCK        8

*/

if (opcode == TCPOPT_SACK

&& opsize >= (TCPOLEN_SACK_BASE

+ TCPOLEN_SACK_PERBLOCK)

&& !((opsize - TCPOLEN_SACK_BASE)

% TCPOLEN_SACK_PERBLOCK)) {

for (i = 0;

i < (opsize - TCPOLEN_SACK_BASE);

i += TCPOLEN_SACK_PERBLOCK) {

tmp = get_unaligned_be32((__be32 *)(ptr+i)+1);

if (after(tmp, *sack))//确认Left小于right,sack指向第一个SACK块

*sack = tmp;

}

return;

}

ptr += opsize - 2;

length -= opsize;

}

}

}

我们看到如果系统支持SACK的选项,会在报文中捎带Permitted Option 选项,在Linux内核中该选项信息定义为宏

#define TCPOPT_SACK_PERM        4

/* SACK Permitted */

D-SACK

RFC2883中对SACK进行了扩展,在SACK中描述的是收到的数据段,这些数据段可以是正常的,也可能是重复发送的,SACK字段具有描述重复发送的数据段的能力,在第一块SACK数据中描述重复接收的不连续数据块的序列号参数,其他SACK数据则描述其他正常接收到的不连续数据,因此第一块SACK描述的序列号会比后面的SACK描述的序列号大;而在接收到不完整的数据段的情况下,SACK范围甚至可能小于当前的ACK值。通过这种方法,发送方可以更仔细判断出当前网络的传输情况,可以发现数据段被网络复制、错误重传、ACK丢失引起的重传、重传超时等异常的网络状况。

SeqNum和Ack是以字节数为单位,所以ack的时候,不能跳着确认,只能确认最大的连续收到的包。

Duplicate SACK – 重复收到数据的问题

Duplicate SACK又称D-SACK,其主要使用了SACK来告诉发送方有哪些数据被重复接收了。RFC-2833 里有详细描述和示例。下面举几个例子(来源于RFC-2833)

D-SACK使用了SACK的第一个段来做标志,

如果SACK的第一个段的范围被ACK所覆盖,那么就是D-SACK

如果SACK的第一个段的范围被SACK的第二个段覆盖,那么就是D-SACK

示例一:ACK丢包

下面的示例中,丢了两个ACK,所以,发送端重传了第一个数据包(3000-3499),于是接收端发现重复收到,于是回了一个SACK=3000-3500,因为ACK都到了4000意味着收到了4000之前的所有数据,所以这个SACK就是D-SACK——旨在告诉发送端我收到了重复的数据,而且我们的发送端还知道,数据包没有丢,丢的是ACK包。

Transmitted

Received    ACK Sent

Segment

Segment     (Including SACK

Blocks)

3000-3499

3000-3499   3500 (ACK dropped)

3500-3999

3500-3999   4000 (ACK dropped)

3000-3499

3000-3499   4000, SACK=3000-3500

---------

示例二,网络延误

下面的示例中,网络包(1000-1499)被网络给延误了,导致发送方没有收到ACK,而后面到达的三个包触发了“Fast Retransmit算法”,所以重传,但重传时,被延误的包又到了,所以,回了一个SACK=1000-1500,因为ACK已到了3000,所以,这个SACK是D-SACK——标识收到了重复的包。

这个案例下,发送端知道之前因为“Fast Retransmit算法”触发的重传不是因为发出去的包丢了,也不是因为回应的ACK包丢了,而是因为网络延时了。

Transmitted

Received    ACK Sent

Segment

Segment     (Including SACK

Blocks)

500-999

500-999     1000

1000-1499

(delayed)

1500-1999

1500-1999   1000, SACK=1500-2000

2000-2499

2000-2499   1000, SACK=1500-2500

2500-2999

2500-2999   1000, SACK=1500-3000

1000-1499

1000-1499   3000

1000-1499   3000, SACK=1000-1500

---------

可见,引入了D-SACK,有这么几个好处:

1)可以让发送方知道,是发出去的包丢了,还是回来的ACK包丢了。

2)是不是自己的timeout太小了,导致重传。

3)网络上出现了先发的包后到的情况(又称reordering)

4)网络上是不是把我的数据包给复制了。

PS:下面通过wireshark抓包工具看看SACK的选项中的block。

linux内核校验tcp包,Linux内核中TCP SACK处理流程分析_第5张图片

PACK#26

linux内核校验tcp包,Linux内核中TCP SACK处理流程分析_第6张图片

PACK#27

linux内核校验tcp包,Linux内核中TCP SACK处理流程分析_第7张图片

PACK#28

linux内核校验tcp包,Linux内核中TCP SACK处理流程分析_第8张图片

PACK#29

linux内核校验tcp包,Linux内核中TCP SACK处理流程分析_第9张图片

PACK#30

linux内核校验tcp包,Linux内核中TCP SACK处理流程分析_第10张图片

PACK#31

通过上面的几张图可以看出,PACK#26、 PACK#27、 PACK#28、 PACK#29、表示为正常的数据段, PACK#31表示是一个DUP-ACK,在该选项信息中包含有可能丢失的段,PACK#30表明传输的数据端和客户端需要的数据段不一致,通过下面的图可以确定:left_edge 和right_edge分别表示的含义。

linux内核校验tcp包,Linux内核中TCP SACK处理流程分析_第11张图片

参考文献:

http://www.ibm.com/developerworks/cn/linux/l-tcp-sack/

http://packetlife.net/blog/2010/jun/17/tcp-selective-acknowledgments-sack/

你可能感兴趣的:(linux内核校验tcp包)