本来周末想搞一下scapy呢,一个python写的交互式数据包编辑注入框架,功能异常强大。然而由于python水平太水,对库的掌握程度完全达不到信手拈来的水平,再加上前些天pending的关于OpenVPN的事情,还有一系列关于虚拟网卡的事情,使我注意到了一个很好用的packetdrill,可以完成本应该由scapy完成的事,恰巧这个东西跟我最近的工作也有关系,就抛弃scapy了,稍微研究了一下它的基本框架,写下本文。
在《通过packetdrill构造的包序列理解TCP快速重传机制》中,我列举了两个脚本,这里讲述后一个脚本的一个细节,我再次把后一个脚本贴如下:
// 建立连接
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
+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
// 在这里,可以用以下的Assert来确认:
0.0 %{
assert tcpi_reordering == 3
assert tcpi_cwnd == 11
}%
// 以下为收到的SACK序列。由于我假设你已经通过上面那个简单的packetdrill脚本理解了SACK和FACK的区别,因此这里我们默认开启FACK!
// sack 1的效果:确认了27001-28001,此处距离ACK字段20001为8个段,超过了reordering 3,会立即触发重传。
+.1 < . 1:1(0) ack 20001 win 257 // ----(sack 1)
+0 < . 1:1(0) ack 20001 win 257 // ----(sack 2)
+0 < . 1:1(0) ack 20001 win 257 // ----(sack 3)
+0 < . 1:1(0) ack 20001 win 257 // ----(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
// 依然可以通过python来确认reordering此时已经不再是3了
// 我们构造同上面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
+0 < . 1:1(0) ack 63001 win 257
+0 < . 1:1(0) ack 63001 win 257
+0 < . 1:1(0) ack 63001 win 257
// 这里,这里到底会不会触发超时重传呢?取决于packetdrill注入下面这个ACK的时机
// 如果没有发生超时重传,下面这个ACK将会再次把reordering从6更新到8
+.1 < . 1:1(0) ack 71001 win 65535
//从这里往后,属于神的世界...
tp->snd_cwnd = min(tp->snd_cwnd, tcp_packets_in_flight(tp) + 1);
来吧,我们依次来算,此时tp->snd_cwnd的值为11,问题是tcp_packets_in_flight是多少?
static inline unsigned int tcp_left_out(const struct tcp_sock *tp)
{
return tp->sacked_out + tp->lost_out;
}
static inline unsigned int tcp_packets_in_flight(const struct tcp_sock *tp)
{
return tp->packets_out - tcp_left_out(tp) + tp->retrans_out;
}
此时:
tcp_for_write_queue_from(skb, sk) {
...
if (tcp_packets_in_flight(tp) >= tp->snd_cwnd)
return;
transmit_skb(skb);
tp->retrans_out++;
}
tp->snd_cwnd = min(tp->snd_cwnd, tcp_packets_in_flight(tp) + 1);
这是为了不往早已无能为力的网络上再添堵,也就是说,当你确定in_flight肯定比当前Rate Halving降窗操作之后的窗口值小的时候,拥塞窗口的值一定是in_flight加1!这也就是传说中的数据包守恒,大多数情况都是如此,Rate Halving(它是一个本地作用)的效用远远不比in_flight表征的实际网络情况(这是一个综合作用)。因此你会发现,大多数情况下,在TCP快速恢复阶段,都是一个一个段重传的。然而RH移植了上游patch后,它使用的是PRR降窗算法,下面我们来算一下PRR算法下,应该重传几个段。
static void tcp_cwnd_reduction(struct sock *sk, int newly_acked_sacked, int fast_rexmit)
{
struct tcp_sock *tp = tcp_sk(sk);
int sndcnt = 0;
int delta = tp->snd_ssthresh - tcp_packets_in_flight(tp);
tp->prr_delivered += newly_acked_sacked;
if (tcp_packets_in_flight(tp) > tp->snd_ssthresh) {
... // in_flight此为2,而ssthresh的值却是5(我使用了Reno,其实Cubic也一样,11*0.7=?)
} else {
sndcnt = min(delta, max(tp->prr_delivered - tp->prr_out, newly_acked_sacked) + 1);
}
tp->snd_cwnd = tcp_packets_in_flight(tp) + sndcnt;
}