ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

通过packetdrill构造的包序列理解TCP快速重传机制

2019-02-17 09:50:13  阅读:712  来源: 互联网

标签:... 重传 ack win +. TCP packetdrill SACK


TCP的逻辑是极其复杂的,其学习曲线虽然很平缓但其每一步都是异常艰难,好在这些都是体力活,只要肯花时间也就不在话下了。想彻底理解一个TCP的机制,有个四部曲:
1.读与其相关的RFC;
2.看Linux协议栈的TCP实现;
3.通过抓包以及其它工具来确认事实就是如此;
4.解决一个与之相关的网络问题。
经历了以上四步骤,相信任何人都可以在相关领域内稍微装逼一把了...
        本文的内容是TCP快速重传机制,但是与其它文章不同的是,本文并不剖析源码实现,也不翻译RFC,更不是原理性介绍,而是通过一个TCP报文序列来看TCP到底是怎么运作的,在可能的情况下,我会对照Linux协议栈源码。
        我们知道,TCP是一个与全世界相关的混沌系统,但是其最终还是要落实到每一个细节,这些细节都是有章可循的,最终就是RFC的规范,在这些微观的细节章法的范围内,TCP在宏观上才得以表现为一个让人琢磨不透的混沌系统。
        理解一个机制最好的办法就是实例讲解,那么随便从网上抓一个报文序列来分析就再好不过,但是这只是事情的一个方面,事情的另一个方面就是,一定要可重现!正是由于TCP在宏观上是一个混沌系统,才让现实中无法重现任意一个TCP报文序列。因此我们必须手工构造报文序列。
        可以做到构造报文序列的工具有很多,比较了一下,我还是选择了packetdrill,这是google的一个工具,比较好用,其不足在于在不同的内核版本上可能会有不同的怪异的问题,正如其社区所承认的,他们没有时间去维护一个放之四海而皆准的稳定版本,但是这无所谓,小白bug自己修复便是,没有修复过bug的程序员是经理。关于packetdrill的细节我就不再赘述,自行到其github上去了解便是,本文马上开始步入正题。
        在行文继续之前,我先说明一个细节。

        由于packetdrill是在运行时才打开一个tun(由tun.ko驱动的一个虚拟网卡设备)网卡设备,然后对其配置,为了仅仅论述TCP本身,我需要消除任何offload机制的影响,并且我不知道怎么用参数去掉offload(好像也没有!),因此我修改了packetdrill的netdev.c的代码:

 

/* Set the offload flags to be like a typical ethernet device */
static void set_device_offload_flags(struct local_netdev *netdev)
{
#ifdef linux
//      const u32 offload =
//          TUN_F_CSUM | TUN_F_TSO4 | TUN_F_TSO6 | TUN_F_TSO_ECN | TUN_F_UFO;
//      if (ioctl(netdev->tun_fd, TUNSETOFFLOAD, offload) != 0)
//              die_perror("TUNSETOFFLOAD");
#endif
}

 

很傻比的一个修改,特此声明。

        首先我给出我的第一个packetdrill脚本,注意看其中的注释。

// 建立连接
0   socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0  setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0

+0  bind(3, ..., ...) = 0
+0  listen(3, 1) = 0

// 完成三次握手
+0  < S 0:0(0) win 32792 <mss 1000,sackOK,nop,nop,nop,wscale 7>
+0  > S. 0:0(0) ack 1 <...>
+.1 < . 1:1(0) ack 1 win 32792
+0  accept(3, ..., ...) = 4

// 发送一个段,注意,它不会诱发拥塞窗口增加,因为初始拥塞窗口已经遵循google的建议变为10个段,此不满10个段会被限制。
// 请用tcpprobe来确认,限制窗口增加的代码请参加本脚本后面的代码。注意,在拥塞窗口从1,2或者3起步的年代,这种限制是不存在的。
+0  write(4, ..., 1000) = 1000
// 发包序列为:+0  > P. 1:1001(1000) ack 1

+.1 < . 1:1(0) ack 1001 win 32792

// 写入4个段,此目的为了触发快速重传
+0  write(4, ..., 4000) = 4000

// 我们得到3个SACK,但是请注意,FACK和标准的SACK的区别要体现了:
// 标准SACK:只有等到4001-5001这一个段以及2001-4001这两个段都收被SACK了,才能触发重传
// FACK:注意到先收到的那个4001-5001 SACK,其与UNA的距离已经超过了3,不用收到2001-5001这个SACK即可触发重传
+.1 < . 1:1(0) ack 1001 win 257 <sack 4001:5001,nop,nop>
+0  < . 1:1(0) ack 1001 win 257 <sack 2001:4001,nop,nop>
// 一个小问题,如果SACK的序列成了以下的:+0  < . 1:1(0) ack 1001 win 257 <sack 2001:3001,nop,nop> 重传还会被触发吗?
// 此时抓包的话,就会发现,重传已经发生

// 全部确认
+.1 < . 1:1(0) ack 5001 win 257


因为这个脚本过于简单,注释里已经给出了一切,因此在文中就不再抓包确认,有兴趣的可以自行确认Reno,SACK,FACK之间的区别,方法很简单,分别开启关闭sack,sack即可:
net.ipv4.tcp_sack = 1|0
net.ipv4.tcp_fack = 1|0

然后抓包来看重传触发的时机,如果你还不是很理解其不同,请一定要做上述的抓包确认,保证可以瞬间搞明白。关于注释里说的那个限制窗口增加的代码如下:

 

int tcp_is_cwnd_limited(const struct sock *sk, u32 in_flight)
{
    const struct tcp_sock *tp = tcp_sk(sk);
    u32 left;
    if (in_flight >= tp->snd_cwnd)
        return 1;

    left = tp->snd_cwnd - in_flight;
    if (sk_can_gso(sk) &&
        left * sysctl_tcp_tso_win_divisor < tp->snd_cwnd &&
        left * tp->mss_cache < sk->sk_gso_max_size)
        return 1;
    return left <= tcp_max_burst(tp);
}

这个函数会在增窗前被调用。

        下面我用一个综合的复杂点的例子来作为本文接下来的内容,这个例子中不仅仅包含了快速重传,还有关于拥塞窗口的部分细节,请一定仔细分析。这个比较复杂些的例子如下:

 

// 建立连接
0   socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0  setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0

+0  bind(3, ..., ...) = 0
+0  listen(3, 1) = 0

// 完成握手
+0  < S 0:0(0) win 65535 <mss 1000,sackOK,nop,nop,nop,wscale 7>
+0  > S. 0:0(0) ack 1 <...>
+.1 < . 1:1(0) ack 1 win 65535
+0  accept(3, ..., ...) = 4

// 发送1个段,不会诱发拥塞窗口增加
+0  write(4, ..., 1000) = 1000
+.1 < . 1:1(0) ack 1001 win 65535

// 再发送1个段,拥塞窗口还是初始值10!
+0  write(4, ..., 1000) = 1000
+.1 < . 1:1(0) ack 2001 win 65535

// .....
+0  write(4, ..., 1000) = 1000
+.1 < . 1:1(0) ack 3001 win 65535

// 不管怎么发,只要是每次发送不超过init_cwnd-reordering,拥塞窗口就不会增加,详见上述的tcp_is_cwnd_limited函数
+0  write(4, ..., 1000) = 1000
+.1 < . 1:1(0) ack 4001 win 65535

// 多发一点,结果呢?自己用tcpprobe确认吧
+0  write(4, ..., 6000) = 6000
+.1 < . 1:1(0) ack 10001 win 65535

// 好吧,我们发送10个段,可以用tcpprobe确认,在收到ACK后拥塞窗口会增加1,这正是慢启动的效果!
+0  write(4, ..., 10000) = 10000
+.1 < . 1:1(0) ack 20001 win 65535

// 该步入正题了。为了触发快速重传,我们发送足够多的数据,一下子发送8个段吧,注意,此时的拥塞窗口为11!
+0  write(4, ..., 8000) = 8000

// 以下为收到的SACK序列。由于我假设你已经通过上面那个简单的packetdrill脚本理解了SACK和FACK的区别,因此这里我们默认开启FACK!
// sack 1的效果:确认了27001-28001,此处距离ACK字段20001为8个段,超过了reordering 3,会立即触发重传。
+.1 < . 1:1(0) ack 20001 win 257 <sack 27001:28001,nop,nop>                                         // ----(sack 1)
+0  < . 1:1(0) ack 20001 win 257 <sack 22001:23001 27001:28001,nop,nop>                             // ----(sack 2)
+0  < . 1:1(0) ack 20001 win 257 <sack 23001:24001 22001:23001 27001:28001,nop,nop>                 // ----(sack 3)
+0  < . 1:1(0) ack 20001 win 257 <sack 24001:25001 23001:24001 22001:23001 27001:28001,nop,nop>     // ----(sack 4)

// 收到了28001的ACK,注意,此时的reordering已经被更新为6了,另外,这个ACK也会尝试触发reordering的更新,但是并不成功,为什么呢?详情见下面的分析。
+.1 < . 1:1(0) ack 28001 win 65535

// 由于经历了上述的快速重传/快速恢复,拥塞窗口已经下降到了5,为了确认reordering已经更新,我们需要将拥塞窗口增加到10或者11
+0  write(4, ..., 5000) = 5000
+.1 < . 1:1(0) ack 33001 win 65535

// 由于此时拥塞窗口的值为5,我们连续写入几个等于拥塞窗口大小的数据,诱发拥塞窗口增加到10.
+0  write(4, ..., 5000) = 5000
+.1 < . 1:1(0) ack 38001 win 65535

+0  write(4, ..., 5000) = 5000
+.1 < . 1:1(0) ack 43001 win 65535

+0  write(4, ..., 5000) = 5000
+.1 < . 1:1(0) ack 48001 win 65535

+0  write(4, ..., 5000) = 5000
+.1 < . 1:1(0) ack 53001 win 65535

+0  write(4, ..., 5000) = 5000
+.1 < . 1:1(0) ack 58001 win 65535

+0  write(4, ..., 5000) = 5000
+.1 < . 1:1(0) ack 63001 win 65535

// 好吧!此时重复上面发生SACK的序列,写入8个段,我们来看看同样的SACK序列还会不会诱发快速重传!
+0  write(4, ..., 8000) = 8000

// 我们构造同上面sack 1/2/3/4一样的SACK序列,然而等待我们的不是重传被触发,而是...
// 什么?没有触发重传?这不可能吧!你看,70001-71001这个段距离63001为8个段,而此时reordering被更新为6,8>6,依然符合触发条件啊,为什么没有触发呢?
// 答案在于,在于8>6触发快速重传有个前提,那就是开启FACK,然而在reordering被更新的时候,已经禁用了FACK,此后就是要数SACK的段数而不是数最高被SACK的段值了,以下4个SACK只是选择确认了4个段,而4<6,不会触发快速重传。
+.1 < . 1:1(0) ack 63001 win 257 <sack 70001:71001,nop,nop>
+0  < . 1:1(0) ack 63001 win 257 <sack 65001:66001 70001:71001,nop,nop>
+0  < . 1:1(0) ack 63001 win 257 <sack 67001:68001 65001:66001 70001:71001,nop,nop>
+0  < . 1:1(0) ack 63001 win 257 <sack 68001:69001 67001:68001 65001:66001 70001:71001,nop,nop>

// 这里,这里到底会不会触发超时重传呢?取决于packetdrill注入下面这个ACK的时机
// 如果没有发生超时重传,下面这个ACK将会再次把reordering从6更新到8
+.1 < . 1:1(0) ack 71001 win 65535

//从这里往后,属于神的世界...

然后在我们来看抓包结果之前,允许我再次重申,如果不是为了分析协议的细节,请直接使用tcpdump的屏幕输出即可,没必要用wireshark/tshark来展现自己的高大上,我们直接看tcpdump的输出来确认细节:

 


 

很明了,如果你去仔细比Linux内核协议栈的实现,会更加清晰,那么在同一TCP连接的后面呈现相同的SACK序列时,情况就不同了,没有触发快速重传,下图分析之:

 



最终呢?我还是发现上面的论述没有一个归纳性的结论,好像一个实例解析一样,这个导致我很失望!不过我还是想归纳一下关于reordering更新的两个细节:
1.SACK导致的reordering更新
if (skb 没有被选择确认 && skb没有被重传过)
{
    if (skb的序列号<最高被SACK的序列号)
        更新reordering为最高被SACK的skb序列号-当前skb的序列号
}

2.ACK导致的reordering更新
if (skb 没有被选择确认 && skb没有被重传过)
{
    if (skb的序列号<最高被SACK的序列号)
        更新reordering为最高被SACK的skb序列号-当前skb的序列号
}

又复制粘贴了,其实上面两种情况是一样的!不管是选择确认还是ACK确认,只要倒序确认,都有可能证明网络存在乱序,只是是否真的乱序要有一个度,这个度就是由reordering来度量的,爆炸!
        总结一下上面的这个packetdrill脚本解释的问题:
1.拥塞窗口的升降机制。初始窗口定为10后的慢启动行为以及降窗行为。
2.标准SACK以及FACK的快速重传触发时机。
3.TCP连接内的reordering的更新机制以及其与FACK的关系。
4.在触发快速重传时的TCP内部维护的各个计数器之间的关系。


到此,本文也该结束了,现在时间是2016/07/16 10:32,距离起床已经过了6个小时,本文写的比较慢,原因在于中间给小小做了早饭,然后又修了空调...
最后,如果说你不知道packetdrill的工作机制,也不必慌张,如果你已经懂了packetdrill的工作机制,也不必张狂,这些都是无关紧要的。

再分享一下我老师大神的人工智能教程吧。零基础!通俗易懂!风趣幽默!还带黄段子!希望你也加入到我们人工智能的队伍中来!https://blog.csdn.net/jiangjunshow

标签:...,重传,ack,win,+.,TCP,packetdrill,SACK
来源: https://www.cnblogs.com/ksiwnhiwhs/p/10390197.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有