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

SACK介绍:

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

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

86d7f39a06769b8a054bf100f3e33bb2.png

即使包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函数中。

8fd449c9ab6fad285b5756685a43f661.png

在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的结构描述。

16f57389beac6b87775073cc7eb9fc49.png

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

995c597061d097cbb27a7e498fc4acfb.png

通过上面的报文格式可以看到,其表现形式符合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。

337ff8227cea155681ceb08c1fbe21c4.png

PACK#26

b5a8a2220fccd8c024c330e58969cfe4.png

PACK#27

76d4b95675fb0afc42144f669db1bf87.png

PACK#28

05780c3ab408cd4320f1590d8cda9aff.png

PACK#29

f49355b303e92690cfb31ec90618ca8b.png

PACK#30

725178d4ce6b1b8db6082e709178dd15.png

PACK#31

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

4b6cfb423efe033270aade2a15cd09ed.png

参考文献:

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要优化 Linux 内核以减少 TCP 重传和重复确认问题,可以尝试以下方法: 1. 调整 TCP 参数:可以通过修改内核参数来调整 TCP 的行为。一些相关的参数括: - tcp_retries1 和 tcp_retries2:控制 TCP 重传的次数。可以适度增加这两个参数的值,以允许更多次的重传尝试。 - tcp_syn_retries:控制 TCP 建立连接时的重传次数。同样,适度增加该参数的值可以增加重传次数。 - tcp_keepalive_time:设置 TCP 连接的空闲时间,超过该时间后会发送 keepalive 报文以保持连接。适当调整该参数可以减少连接超时导致的重传。 - tcp_timestamps:启用 TCP 时间戳选项,可以减少重传和重复确认的问题。 2. 启用 SACK(Selective Acknowledgment):SACK 可以使 TCP 在接收到乱序报文时,只对缺失的报文进行重传,而不是对整个连续的报文段进行重传。在内核启用 SACK 可以减少不必要的重传和重复确认。 3. 调整 TCP 拥塞控制算法:Linux 内核提供了多种拥塞控制算法,如 Reno、Cubic、BIC 等。不同的算法可能适用于不同的网络环境和负载条件。可以尝试切换拥塞控制算法来优化 TCP 的性能和减少重传问题。 4. 更新内核版本:及时更新 Linux 内核版本,以获取更好的 TCP 实现和修复已知的问题。新版本的内核通常会对 TCP 进行改进和优化。 5. 检查网络设备和链路:确保网络设备(如网卡、交换机、路由器)的驱动程序和固件是最新版本,并检查链路的稳定性和性能。有时,问题可能出现在网络设备或链路上,而不是在内核本身。 6. 使用专业的网络优化工具:有一些专业的网络优化工具可用于调整和优化 TCP 参数,例如 sysctl、tune2fs、ethtool 等。这些工具可以提供更精细的控制和配置选项。 需要注意的是,优化内核时应该谨慎操作,并在测试环境进行验证。不同的应用程序和网络环境可能需要不同的优化策略。建议在进行任何更改之前备份重要的配置文件和系统状态,并监控性能变化以确保优化的有效性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值