SACK 介绍:
SACK:Selective Acknowledgment (SACK)(参看),这种方式需要在TCP头里加一个SACK的东西,ACK还是Fast
Retransmit的ACK,SACK则是汇报收到的数据碎版。参看下图:
即使包 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函数中。
在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的结构描述。
下面是通过Wireshark抓包工具,抓取到的含有TCP SACK的报文格式。
通过上面的报文格式可以看到,其表现形式符合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。
PACK#26
PACK#27
PACK#28
PACK#29
PACK#30
PACK#31
通过上面的几张图可以看出,PACK#26、 PACK#27、 PACK#28、 PACK#29、表示为正常的数据段, PACK#31表示是一个DUP-ACK,在该选项信息中包含有可能丢失的段,PACK#30表明传输的数据端和客户端需要的数据段不一致,通过下面的图可以确定:left_edge 和right_edge分别表示的含义。
参考文献:
http://www.ibm.com/developerworks/cn/linux/l-tcp-sack/
http://packetlife.net/blog/2010/jun/17/tcp-selective-acknowledgments-sack/
阅读(1071) | 评论(0) | 转发(0) |