在时间戳选项诞生之前,TCP有三个问题难以解决:
(1)通信延迟RTT(Round Trip Time)测量
RTT对于拥塞控制是十分重要的(比如计算多长时间重传数据)。通常,测量RTT的方法是发送一个报文,记录发送时间t1;当收到这个报文的确认时记录时间t2,t2 - t1就可以得到RTT。但TCP使用延迟确认机制,而且ACK可能会丢失,使得收到ACK时也无法确定是对哪个报文的回应。
(2)序列号快速回绕
TCP判断数据是新是旧的方法是检查数据的序列号是否位于sun.una到sun.una + 2**31的范围内,而序列号空间的总大小为2*32,即约4.29G。在万兆局域网中,4.29G字节数据回绕只需几秒钟,这时TCP就无法准确判断数据的新旧。
(3)SYN Cookie的选项信息
TCP开启SYN Cookie功能时由于Server在收到SYN请求后不保存连接,故SYN包中携带的选项(WScale、SACK)无法保存,当SYN Cookie验证通过、新连接建立之后,这些选项都无法开启。
使用时间戳选项就可以解决上述问题。
问题(1)解决方法:发送一个报文时将发送时间写入时间戳选项,在收到的ACK报文时通过其时间戳选项的回显值就能知道它确认的是什么时候发送的报文,用当前时间减去回显时间就可以得到一个RTT。
问题(2)解决方法:收到一个报文时记录选项中的时间戳值,收到下一个报文时将其中的时间戳与上次的进行对比即可。时间戳回绕的速度只与对端主机时钟频率有关。Linux以本地时钟计数(jiffies)作为时间戳的值,假设时钟计数加1需要1ms,则需要约24.8天才能回绕一半,只要报文的生存时间小于这个值的话判断新旧数据就不会出错,这个功能被称为PAWS(Protect Against Wrapped Sequence numbers)。这样虽然可以解决问题(2),但随着硬件时钟频率的提升,时间戳回绕的速度也会加快,用时间戳解决序列号回绕问题的方法早晚会遇到困境。
问题(3)解决方法:将WScale和SACK选项信息编码进32 bit的时间戳值中,建立连接时会收到ACK报文,将报文的时间戳选项的回显信息解码就可以还原WScale和SACK信息(这部分内容见《3.6 SYN Cookie》)。
时间戳选项格式:
其中,TSval是本端填写的时间戳,TSecr是回显给对端的时间戳。两端必须都分别在SYN包和SYN|ACK包中开启时间戳选项,时间戳功能才能生效。
SYN包的时间戳选项在tcp_transmit_skb调用tcp_syn_options函数时设置:
498 static unsigned int tcp_syn_options(struct sock *sk, struct sk_buff *skb,
499 struct tcp_out_options *opts,
500 struct tcp_md5sig_key **md5)
501 {
502 struct tcp_sock *tp = tcp_sk(sk);
503 unsigned int remaining = MAX_TCP_OPTION_SPACE;
504 struct tcp_fastopen_request *fastopen = tp->fastopen_req;
505
506 #ifdef CONFIG_TCP_MD5SIG
507 *md5 = tp->af_specific->md5_lookup(sk, sk);
508 if (*md5) {
509 opts->options |= OPTION_MD5;
510 remaining -= TCPOLEN_MD5SIG_ALIGNED;
511 }
512 #else
513 *md5 = NULL;
514 #endif
...
528 if (likely(sysctl_tcp_timestamps && *md5 == NULL)) { //开启MD5选项就不能使用时间戳,why?
529 opts->options |= OPTION_TS;
530 opts->tsval = TCP_SKB_CB(skb)->when + tp->tsoffset; //设置时间戳
531 opts->tsecr = tp->rx_opt.ts_recent; //设置回显值
532 remaining -= TCPOLEN_TSTAMP_ALIGNED;
533 }
...
SYN|ACK报文的时间戳是在tcp_synack_options选项中设置:
560 static unsigned int tcp_synack_options(struct sock *sk,
561 struct request_sock *req,
562 unsigned int mss, struct sk_buff *skb,
563 struct tcp_out_options *opts,
564 struct tcp_md5sig_key **md5,
565 struct tcp_fastopen_cookie *foc)
566 {
567 struct inet_request_sock *ireq = inet_rsk(req);
568 unsigned int remaining = MAX_TCP_OPTION_SPACE;
569
570 #ifdef CONFIG_TCP_MD5SIG
571 *md5 = tcp_rsk(req)->af_specific->md5_lookup(sk, req);
572 if (*md5) {
573 opts->options |= OPTION_MD5;
574 remaining -= TCPOLEN_MD5SIG_ALIGNED;
575
576 /* We can't fit any SACK blocks in a packet with MD5 + TS
577 * options. There was discussion about disabling SACK
578 * rather than TS in order to fit in better with old,
579 * buggy kernels, but that was deemed to be unnecessary.
580 */
581 ireq->tstamp_ok &= !ireq->sack_ok;
582 }
583 #else
584 *md5 = NULL;
585 #endif
...
596 if (likely(ireq->tstamp_ok)) { //对端开启时间戳
597 opts->options |= OPTION_TS;
598 opts->tsval = TCP_SKB_CB(skb)->when;
599 opts->tsecr = req->ts_recent;
600 remaining -= TCPOLEN_TSTAMP_ALIGNED;
601 }
...
非SYN包中的时间戳选项由tcp_transmit_skb调用tcp_established_options函数填写:
623 static unsigned int tcp_established_options(struct sock *sk, struct sk_buff *skb,
624 struct tcp_out_options *opts,
625 struct tcp_md5sig_key **md5)
626 {
627 struct tcp_skb_cb *tcb = skb ? TCP_SKB_CB(skb) : NULL;
628 struct tcp_sock *tp = tcp_sk(sk);
629 unsigned int size = 0;
630 unsigned int eff_sacks;
...
642 if (likely(tp->rx_opt.tstamp_ok)) { //对端开启时间戳选项
643 opts->options |= OPTION_TS;
644 opts->tsval = tcb ? tcb->when + tp->tsoffset : 0;
645 opts->tsecr = tp->rx_opt.ts_recent;
646 size += TCPOLEN_TSTAMP_ALIGNED;
647 }
...
时间戳选项的值opts->tsval
由TCP_SKB_CB(skb)->when和tp->tsoffset构成(SYN|ACK的只有前者,why?),其中tp->tsoffset为用户使用TCP_TIMESTAMP socket选项设置的偏移值,TCP_SKB_CB(skb)->when为skb发送的时间:
1811 static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
1812 int push_one, gfp_t gfp)
1813 {
...
1882 TCP_SKB_CB(skb)->when = tcp_time_stamp;
...
tcp_time_stamp就是Linux的jiffies:
674 #define tcp_time_stamp ((__u32)(jiffies))
tcp_options_write函数将选项写入报文:
409 static void tcp_options_write(__be32 *ptr, struct tcp_sock *tp,
410 struct tcp_out_options *opts)
411 {
412 u16 options = opts->options; /* mungable copy */
...
428 if (likely(OPTION_TS & options)) {
429 if (unlikely(OPTION_SACK_ADVERTISE & options)) { //如果有SACK_PERMIT选项则将其与时间戳选项写在一起以便节省空间
430 *ptr++ = htonl((TCPOPT_SACK_PERM << 24) |
431 (TCPOLEN_SACK_PERM << 16) |
432 (TCPOPT_TIMESTAMP << 8) |
433 TCPOLEN_TIMESTAMP);
434 options &= ~OPTION_SACK_ADVERTISE;
435 } else {
436 *ptr++ = htonl((TCPOPT_NOP << 24) |
437 (TCPOPT_NOP << 16) |
438 (TCPOPT_TIMESTAMP << 8) |
439 TCPOLEN_TIMESTAMP);
440 }
441 *ptr++ = htonl(opts->tsval); //写入时间戳值
442 *ptr++ = htonl(opts->tsecr); //写入回显值
443 }
...
收到报文时
TCP使用tcp_parse_options函数解析时间戳选项:
3481 void tcp_parse_options(const struct sk_buff *skb,
3482 struct tcp_options_received *opt_rx, int estab,
3483 struct tcp_fastopen_cookie *foc)
3484 {
3485 const unsigned char *ptr;
3486 const struct tcphdr *th = tcp_hdr(skb);
3487 int length = (th->doff * 4) - sizeof(struct tcphdr);
...
3534 case TCPOPT_TIMESTAMP:
3535 if ((opsize == TCPOLEN_TIMESTAMP) &&
3536 ((estab && opt_rx->tstamp_ok) || //建立状态且时间戳已经开启
3537 (!estab && sysctl_tcp_timestamps))) { //非建立状态但时间戳允许开启
3538 opt_rx->saw_tstamp = 1; //发现时间戳选项
3539 opt_rx->rcv_tsval = get_unaligned_be32(ptr);
3540 opt_rx->rcv_tsecr = get_unaligned_be32(ptr + 4);
3541 }
3542 break;
...
另外,
tcp_parse_aligned_timestamp函数用于快速解析报文中只有时间戳选项时的时间戳值:
3591 static bool tcp_parse_aligned_timestamp(struct tcp_sock *tp, const struct tcphdr *th)
3592 {
3593 const __be32 *ptr = (const __be32 *)(th + 1);
3594
3595 if (*ptr == htonl((TCPOPT_NOP << 24) | (TCPOPT_NOP << 16)
3596 | (TCPOPT_TIMESTAMP << 8) | TCPOLEN_TIMESTAMP)) {
3597 tp->rx_opt.saw_tstamp = 1;
3598 ++ptr;
3599 tp->rx_opt.rcv_tsval = ntohl(*ptr);
3600 ++ptr;
3601 if (*ptr)
3602 tp->rx_opt.rcv_tsecr = ntohl(*ptr) - tp->tsoffset;
3603 else
3604 tp->rx_opt.rcv_tsecr = 0;
3605 return true;
3606 }
3607 return false;
3608 }
收到SYN或SYN|ACK时如果发现有时间戳选项则会设置tp->opt_rx.tstamp_ok为1。
收到SYN时:
1465 int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
1466 {
1467 struct tcp_options_received tmp_opt;
...
1517 tcp_parse_options(skb, &tmp_opt, 0, want_cookie ? NULL : &foc);
1518
1519 if (want_cookie && !tmp_opt.saw_tstamp) //需要使用SYN Cookie但时间戳选项未开启
1520 tcp_clear_options(&tmp_opt); //清除选项信息,因为没法保存
1521
1522 tmp_opt.tstamp_ok = tmp_opt.saw_tstamp;
1523 tcp_openreq_init(req, &tmp_opt, skb); //将tmp_opt中的信息转存入request_sock中
...
tcp_openreq_init:
1075 static inline void tcp_openreq_init(struct request_sock *req,
1076 struct tcp_options_received *rx_opt,
1077 struct sk_buff *skb)
1078 {
...
1087 req->ts_recent = rx_opt->saw_tstamp ? rx_opt->rcv_tsval : 0;
1088 ireq->tstamp_ok = rx_opt->tstamp_ok;
...
收到SYN|ACK时:
5373 static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
5374 const struct tcphdr *th, unsigned int len)
5375 {
...
5456 if (tp->rx_opt.saw_tstamp) {
5457 tp->rx_opt.tstamp_ok = 1;
5458 tp->tcp_header_len =
5459 sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED; //TCP头长度加上时间戳选项长度
5460 tp->advmss -= TCPOLEN_TSTAMP_ALIGNED; //MSS减去时间戳选项长度
5461 tcp_store_ts_recent(tp); //记录时间戳的值用于回显
...
tcp_store_ts_recent:
3271 static void tcp_store_ts_recent(struct tcp_sock *tp)
3272 {
3273 tp->rx_opt.ts_recent = tp->rx_opt.rcv_tsval; //用于回显,同时也用于PAWS
3274 tp->rx_opt.ts_recent_stamp = get_seconds(); //保存记录的时间
3275 }
先来看看Linux TCP是如果利用时间戳解决问题(1)的。TCP接收数据时使用tcp_rcv_rtt_measure_ts函数用来计算RTT:
459 static void tcp_rcv_rtt_update(struct tcp_sock *tp, u32 sample, int win_dep)
460 {
461 u32 new_sample = tp->rcv_rtt_est.rtt;
462 long m = sample;
463
464 if (m == 0)
465 m = 1;
466
467 if (new_sample != 0) {
468 /* If we sample in larger samples in the non-timestamp
469 * case, we could grossly overestimate the RTT especially
470 * with chatty applications or bulk transfer apps which
471 * are stalled on filesystem I/O.
472 *
473 * Also, since we are only going for a minimum in the
474 * non-timestamp case, we do not smooth things out
475 * else with timestamps disabled convergence takes too
476 * long.
477 */
478 if (!win_dep) {
479 m -= (new_sample >> 3);
480 new_sample += m;
481 } else {
482 m <<= 3;
483 if (m < new_sample)
484 new_sample = m;
485 }
486 } else {
487 /* No previous measure. */
488 new_sample = m << 3;
489 }
490
491 if (tp->rcv_rtt_est.rtt != new_sample)
492 tp->rcv_rtt_est.rtt = new_sample;
493 }
...
508 static inline void tcp_rcv_rtt_measure_ts(struct sock *sk,
509 const struct sk_buff *skb)
510 {
511 struct tcp_sock *tp = tcp_sk(sk);
512 if (tp->rx_opt.rcv_tsecr && //对端回显的时间戳,可以看做数据发送时间
513 (TCP_SKB_CB(skb)->end_seq -
514 TCP_SKB_CB(skb)->seq >= inet_csk(sk)->icsk_ack.rcv_mss)) //skb中的数据大于一个MSS,意味着数据稳定传输
515 tcp_rcv_rtt_update(tp, tcp_time_stamp - tp->rx_opt.rcv_tsecr, 0); //当前时间可以看做数据接收时间,用当前时间减去数据发送的时间去更新RTT
516 }
收到ACK时在tcp_clean_rtx_queue函数中使用tcp_ack_update_rtt来更新RTT:
3001 static int tcp_clean_rtx_queue(struct sock *sk, int prior_fackets,
3002 u32 prior_snd_una)
3003 {
...
3095 if (flag & FLAG_ACKED) {
...
3104 tcp_ack_update_rtt(sk, flag, seq_rtt); //用当前时间减去数据发送的时间去更新RTT
...
tcp_ack_update_rtt用有时间戳和没有时间戳两种方式更新RTT:
2866 static void tcp_ack_saw_tstamp(struct sock *sk, int flag)
...
2883 struct tcp_sock *tp = tcp_sk(sk);
2884
2885 tcp_valid_rtt_meas(sk, tcp_time_stamp - tp->rx_opt.rcv_tsecr);
2886 }
...
2905 static inline void tcp_ack_update_rtt(struct sock *sk, const int flag,
2906 const s32 seq_rtt)
2907 {
2908 const struct tcp_sock *tp = tcp_sk(sk);
2909 /* Note that peer MAY send zero echo. In this case it is ignored. (rfc1323) */
2910 if (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr) //有时间戳选项且回显值非0
2911 tcp_ack_saw_tstamp(sk, flag);
2912 else if (seq_rtt >= 0)
2913 tcp_ack_no_tstamp(sk, seq_rtt, flag);
2914 }
再来看问题(2)。收到包时
TCP在tcp_rcv_established函数中会检查时间戳:
5076 int tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
5077 const struct tcphdr *th, unsigned int len)
5078 {
5079 struct tcp_sock *tp = tcp_sk(sk);
...
5119 /* Check timestamp */
5120 if (tcp_header_len == sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) { //TCP报头中只有时间戳选项
5121 /* No? Slow path! */
5122 if (!tcp_parse_aligned_timestamp(tp, th)) //快速解析时间戳选项失败?
5123 goto slow_path;
5124
5125 /* If PAWS failed, check it more carefully in slow path */
5126 if ((s32)(tp->rx_opt.rcv_tsval - tp->rx_opt.ts_recent) < 0) //当前包的时间戳小于上次收到的包的时间戳
5127 goto slow_path; //并不意味着一定是旧包,需仔细检查
5128
5129 /* DO NOT update ts_recent here, if checksum fails
5130 * and timestamp was corrupted part, it will result
5131 * in a hung connection since we will drop all
5132 * future packets due to the PAWS test.
5133 */
5134 }
...
5251 slow_path:
...
5262 if (!tcp_validate_incoming(sk, skb, th, 1))
5263 return 0;
...
tcp_validate_incoming函数中会仔细检查时间戳:
3696 static int tcp_disordered_ack(const struct sock *sk, const struct sk_buff *skb)
3697 {
3698 const struct tcp_sock *tp = tcp_sk(sk);
3699 const struct tcphdr *th = tcp_hdr(skb);
3700 u32 seq = TCP_SKB_CB(skb)->seq;
3701 u32 ack = TCP_SKB_CB(skb)->ack_seq;
3702
3703 return (/* 1. Pure ACK with correct sequence number. */
3704 (th->ack && seq == TCP_SKB_CB(skb)->end_seq && seq == tp->rcv_nxt) &&
3705
3706 /* 2. ... and duplicate ACK. */
3707 ack == tp->snd_una &&
3708
3709 /* 3. ... and does not update window. */
3710 !tcp_may_update_window(tp, ack, seq, ntohs(th->window) << tp->rx_opt.snd_wscale) &&
3711
3712 /* 4. ... and sits in replay window. */
3713 (s32)(tp->rx_opt.ts_recent - tp->rx_opt.rcv_tsval) <= (inet_csk(sk)->icsk_rto * 1024) / HZ);
3714 }
3715
3716 static inline bool tcp_paws_discard(const struct sock *sk,
3717 const struct sk_buff *skb)
3718 {
3719 const struct tcp_sock *tp = tcp_sk(sk);
3720
3721 return !tcp_paws_check(&tp->rx_opt, TCP_PAWS_WINDOW) && //TCP_PAWS_WINDOW的值为1
3722 !tcp_disordered_ack(sk, skb);
3723 }
...
4985 static bool tcp_validate_incoming(struct sock *sk, struct sk_buff *skb,
4986 const struct tcphdr *th, int syn_inerr)
4987 {
4988 struct tcp_sock *tp = tcp_sk(sk);
4989
4990 /* RFC1323: H1. Apply PAWS check first. */
4991 if (tcp_fast_parse_options(skb, th, tp) && tp->rx_opt.saw_tstamp &&
4992 tcp_paws_discard(sk, skb)) { //tcp_paws_discard为真意味着PAWS检查失败
4993 if (!th->rst) {
4994 NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSESTABREJECTED);
4995 tcp_send_dupack(sk, skb);
4996 goto discard;
4997 }
4998 /* Reset is accepted even if it did not pass PAWS. */
4999 }
...
tcp_paws_check函数如果返回true则PAWS通过:
1143 static inline bool tcp_paws_check(const struct tcp_options_received *rx_opt,
1144 int paws_win)
1145 {
1146 if ((s32)(rx_opt->ts_recent - rx_opt->rcv_tsval) <= paws_win)
1147 return true; //当前包的时间戳只能比上次收到包的时间戳早少于2个jiffies,再多了就会被认为是旧包
1148 if (unlikely(get_seconds() >= rx_opt->ts_recent_stamp + TCP_PAWS_24DAYS)) //从上次收到包到现在经历的时间多于24天
1149 return true;
1150 /*
1151 * Some OSes send SYN and SYNACK messages with tsval=0 tsecr=0,
1152 * then following tcp messages have valid values. Ignore 0 value,
1153 * or else 'negative' tsval might forbid us to accept their packets.
1154 */
1155 if (!rx_opt->ts_recent) //没有上次收到包的时间戳的记录
1156 return true; //有些OS会在SYN和SYN|ACK中发送0值的时间戳和回显,但在随后的报文中会携带正常的值。对于这种情况还是先让它通过吧
1157 return false;
1158 }
看来,如果一个TCP连接连续24天不收发数据则在接收第一个包时基于时间戳的PAWS会失效,否则当前包就会被当做旧包丢弃。
现在讨论一下基于时间戳的PAWS的潜在问题。如果jiffies 1ms加1,则时间戳回绕需要24.8天;如果jiffies提高到1us加1,则回绕需要约71.58分钟才能回绕,这时问题也不大,因为网络中旧报文几乎不可能生存超过70分钟,只是如果70分钟没有报文收发则会有一个包越过PAWS(这种情况会比较多见,相比之下24天没有数据传输的TCP连接少之又少),但除非这个包碰巧是序列号回绕的旧数据包而被放入接收队列(太巧了吧),否则也不会有问题;但如果主机的时钟频率提高到jiffies每0.1us加1呢?回绕需要7分钟多一点,这时就可能会有问题了:
(1)连接如果7分钟没有数据收发就会有一个报文越过PAWS,对于TCP连接而言这么短的时间内没有数据交互太常见了吧!这样的话会频繁有包越过PAWS检查,从而使得旧包混入数据中的概率大大增加
(2)高时钟频率条件下数据的发送和接收应该都是十分迅速的,TCP序列号的回绕速度应该快于时间戳的回绕速度,而旧报文的网络中的生存时间是否会大于7分钟呢?如果不是,则不会有问题;否则就很危险了。可以考虑以下解决方案:
1)增加时间戳的大小,由32 bit扩大到64bit
这样虽然可以在能够预见的未来解决时间戳回绕的问题,但会导致新旧协议兼容性问题,像现在的IPv4与IPv6一样
2)将一个与时钟频率无关的值作为时间戳,时钟频率可以增加但时间戳的增速不变
随着时钟频率的提高,TCP在相同时间内能够收发的包也会越来越多。如果时间戳的增速不变,则会有越来越多的报文使用相同的时间戳。这种趋势到达一定程度则时间戳就会失去意义,除非在可预见的未来这种情况不会发生。
3)暂时没想到