Chinaunix首页 | 论坛 | 博客
  • 博客访问: 131799
  • 博文数量: 17
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 40
  • 用 户 组: 普通用户
  • 注册时间: 2018-08-08 00:05
文章分类
文章存档

2020年(4)

2019年(10)

2018年(3)

我的朋友

分类: LINUX

2020-02-25 14:20:12

SACK 介绍:

SACK:Selective Acknowledgment (SACK)(参看),这种方式需要在TCP头里加一个SACK的东西,ACK还是Fast RetransmitACKSACK则是汇报收到的数据碎版。参看下图:

        即使包 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选项。默认值为1true)。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()函数中有下面的赋值:


  1. static bool tcp_new(struct nf_conn *ct, const struct sk_buff *skb,
  2.          unsigned int dataoff)
  3. {
  4.     enum tcp_conntrack new_state;
  5.     const struct tcphdr *th;
  6.     struct tcphdr _tcph;
  7.     const struct ip_ct_tcp_state *sender = &ct->proto.tcp.seen[0];
  8.     const struct ip_ct_tcp_state *receiver = &ct->proto.tcp.seen[1];
  9. th = skb_header_pointer(skb, dataoff, sizeof(_tcph), &_tcph);
  10.     BUG_ON(th == NULL);
  11. /* Don't need lock here: this conntrack not in circulation yet */
  12.     new_state
  13.         = tcp_conntracks[0][get_conntrack_index(th)]
  14.         [TCP_CONNTRACK_NONE];

  15.     /* Invalid: delete conntrack */
  16.     if (new_state >= TCP_CONNTRACK_MAX) {
  17.         pr_debug("nf_ct_tcp: invalid new deleting.\n");
  18.         return false;
  19.     }
  20. if (new_state == TCP_CONNTRACK_SYN_SENT) {
  21.         /* SYN packet */
  22.         ct->proto.tcp.seen[0].td_end =
  23.             segment_seq_plus_len(ntohl(th->seq), skb->len,
  24.                      dataoff, th);
  25.         ct->proto.tcp.seen[0].td_maxwin = ntohs(th->window);
  26.         if (ct->proto.tcp.seen[0].td_maxwin == 0)
  27.             ct->proto.tcp.seen[0].td_maxwin = 1;
  28.         ct->proto.tcp.seen[0].td_maxend =
  29.             ct->proto.tcp.seen[0].td_end;

  30.         tcp_options(skb, dataoff, th, &ct->proto.tcp.seen[0]);
  31.         ct->proto.tcp.seen[1].flags = 0;
  32.     } else if (nf_ct_tcp_loose == 0) {
  33.         /* Don't try to pick up connections. */
  34.         return false;
  35.     } else {
  36.         /*
  37.          * We are in the middle of a connection,
  38.          * its history is lost for us.
  39.          * Let's try to use the data from the packet.
  40.          */
  41.         ct->proto.tcp.seen[0].td_end =
  42.             segment_seq_plus_len(ntohl(th->seq), skb->len,
  43.                      dataoff, th);
  44.         ct->proto.tcp.seen[0].td_maxwin = ntohs(th->window);
  45.         if (ct->proto.tcp.seen[0].td_maxwin == 0)
  46.             ct->proto.tcp.seen[0].td_maxwin = 1;
  47.         ct->proto.tcp.seen[0].td_maxend =
  48.             ct->proto.tcp.seen[0].td_end +
  49.             ct->proto.tcp.seen[0].td_maxwin;
  50.         ct->proto.tcp.seen[0].td_scale = 0;

  51.         /* We assume SACK and liberal window checking to handle
  52.          * window scaling */
  53.         ct->proto.tcp.seen[0].flags =
  54.         ct->proto.tcp.seen[1].flags = IP_CT_TCP_FLAG_SACK_PERM |
  55.                      IP_CT_TCP_FLAG_BE_LIBERAL;
  56.     }
  57. 在tcp_in_window函数中有下面的配置
  58. static bool tcp_in_window(const struct nf_conn *ct,
  59.              struct ip_ct_tcp *state,
  60.              enum ip_conntrack_dir dir,
  61.              unsigned int index,
  62.              const struct sk_buff *skb,
  63.              unsigned int dataoff,
  64.              const struct tcphdr *tcph,
  65.              u_int8_t pf)
  66. {
  67.     struct net *net = nf_ct_net(ct);
  68.     struct ip_ct_tcp_state *sender = &state->seen[dir];
  69.     struct ip_ct_tcp_state *receiver = &state->seen[!dir];
  70.     const struct nf_conntrack_tuple *tuple = &ct->tuplehash[dir].tuple;
  71.     __u32 seq, ack, sack, end, win, swin;
  72.     s16 receiver_offset;
  73.     bool res;

  74.     /*
  75.      * Get the required data from the packet.
  76.      */
  77.     seq = ntohl(tcph->seq);
  78.     ack = sack = ntohl(tcph->ack_seq);
  79.     win = ntohs(tcph->window);
  80.     end = segment_seq_plus_len(seq, skb->len, dataoff, tcph);

  81.     if (receiver->flags & IP_CT_TCP_FLAG_SACK_PERM)//根据是否设置了该标志位进行相应的处理
  82.         tcp_sack(skb, dataoff, tcph, &sack);
  83. ……………..
  84. }

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


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



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

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

点击(此处)折叠或打开

  1. static void tcp_sack(const struct sk_buff *skb, unsigned int dataoff,
  2.                      const struct tcphdr *tcph, __u32 *sack)
  3. {
  4.     unsigned char buff[(15 * 4) - sizeof(struct tcphdr)];
  5.     const unsigned char *ptr;
  6.     int length = (tcph->doff*4) - sizeof(struct tcphdr);
  7.     __u32 tmp;

  8.     if (!length)
  9.         return;

  10.     ptr = skb_header_pointer(skb, dataoff + sizeof(struct tcphdr),
  11.                  length, buff);
  12.     BUG_ON(ptr == NULL);

  13.     /* Fast path for timestamp-only option */
  14.     if (length == TCPOLEN_TSTAMP_ALIGNED*4
  15.      && *(__be32 *)ptr == htonl((TCPOPT_NOP << 24)
  16.                  | (TCPOPT_NOP << 16)
  17.                  | (TCPOPT_TIMESTAMP << 8)
  18.                  | TCPOLEN_TIMESTAMP))
  19.         return;

  20.     while (length > 0) {
  21.         int opcode = *ptr++;//获取选项码,
  22.         int opsize, i;

  23.         switch (opcode) {
  24.         case TCPOPT_EOL:
  25.             return;
  26.         case TCPOPT_NOP:    /* Ref: RFC 793 section 3.1 */
  27.             length--;
  28.             continue;
  29.         default:
  30.             opsize = *ptr++;
  31.   //下面的代码主要判断选项长度的合法性
  32.             if (opsize < 2) /* "silly options" */
  33.                 return;
  34.             if (opsize > length)
  35.                 break;    /* don't parse partial options */
  36.        /*下面的代码主要判断,如果选项的code为TCPOPT_SACK表示含有SACK选项信息,并且包含的选项大小大于10,因为 Left Edge of Block 和TCPOLEN_SACK_PERBLOCK各占4个字节+长度1个字节+Code 一个字节,选项中可能含有多个 edge of block,通过for循环确定第一个SACK块。
  37. #define TCPOLEN_SACK_BASE        2
  38. #define TCPOLEN_SACK_PERBLOCK        8
  39. */
  40.             if (opcode == TCPOPT_SACK
  41.              && opsize >= (TCPOLEN_SACK_BASE
  42.                      + TCPOLEN_SACK_PERBLOCK)
  43.              && !((opsize - TCPOLEN_SACK_BASE)
  44.                  % TCPOLEN_SACK_PERBLOCK)) {
  45.                 for (i = 0;
  46.                  i < (opsize - TCPOLEN_SACK_BASE);
  47.                  i += TCPOLEN_SACK_PERBLOCK) {
  48.                     tmp = get_unaligned_be32((__be32 *)(ptr+i)+1);

  49.                     if (after(tmp, *sack))//确认Left小于right,sack指向第一个SACK块
  50.                         *sack = tmp;
  51.                 }
  52.                 return;
  53.             }
  54.             ptr += opsize - 2;
  55.             length -= opsize;
  56.         }
  57.     }
  58. }


我们看到如果系统支持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/
阅读(2085) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~