内核选项tcp_tw_recycle原理

#Linux #TCP

why TIME_WAIT state ?

在TCP状态机中,TIME_WAIT状态主要用来避免延迟的旧连接的数据被相同的新连接(连接相同指SRC IP:PORT DST IP:PORT相同)所接收。

time_wait

如上图所示:

  1. 连接<(IP a : Port p) - (IP b : Port q)>断开
  2. 新连接<(IP a : Port p) - (IP b : Port q)>建立
  3. 上次断开连接的数据在网络中延迟并在新连接建立后到来,这样的数据应该是错误并被避免的

TIME_WAIT状态就是来解决上面这样的问题的,一般TIME_WAIT状态等待的时间等于2MSL(最大报文存活时间的2倍),这样就可以确保旧的延迟的报文不被新连接所接收到。

在Linux具体实现协议栈时,TIME_WAIT状态是保存在每个连接相关的数据(结构TCB即TCP control block,保存该连接所有的状态数据)中的,因此在一些高性能的服务器上,就希望这样的TIME_WAIT状态可以尽量的少(节省服务器内存,并且可以快速的端口重用以建立新连接),所以RFC6191提出了一种基于TCP timestamp的方法来进行快速的TIME_WAIT状态回收。

RFC6191在思路上借鉴了RFC1122,对比RFC1122通过序列号的递增性,RFC6191则通过TCP timestamp来进行快速的TIME_WAIT状态回收,具体思路大致是当一个新连接(SRC IP, SRC PORT, DST IP, DST PORT)到来时,如果有相同的连接处于TIME_WAIT状态,且新连接的SYN报文的timestamp要大于最近该连接上的timestamp时,可以接受该连接建立的请求。即要求了来自同一个连接(4元组)的timestamp是单调递增的。

tcp_tw_recycle内核实现

在内核中,当开启了tcp_tw_recycle选项时,即:

$ echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle

内核TCP连接建立时函数(未贴完):

1440 int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
1441 {
1442         struct tcp_options_received tmp_opt;
1443         struct request_sock *req;
1444         struct inet_request_sock *ireq;
1445         struct tcp_sock *tp = tcp_sk(sk);
1446         struct dst_entry *dst = NULL;
1447         __be32 saddr = ip_hdr(skb)->saddr;
1448         __be32 daddr = ip_hdr(skb)->daddr;
1449         __u32 isn = TCP_SKB_CB(skb)->when;
1450         bool want_cookie = false;
1451         struct flowi4 fl4;
1452         struct tcp_fastopen_cookie foc = { .len = -1 };
1453         struct tcp_fastopen_cookie valid_foc = { .len = -1 };
1454         struct sk_buff *skb_synack;
1455         int do_fastopen;
1456
1457         /* Never answer to SYNs send to broadcast or multicast */
1458         if (skb_rtable(skb)->rt_flags & (RTCF_BROADCAST | RTCF_MULTICAST))
1459                 goto drop;
1460
1461         /* TW buckets are converted to open requests without
1462  * limitations, they conserve resources and peer is
1463  * evidently real one.
1464  */
1465         if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
1466                 want_cookie = tcp_syn_flood_action(sk, skb, "TCP");
1467                 if (!want_cookie)
1468                         goto drop;
1469         }
1470
1471         /* Accept backlog is full. If we have already queued enough
1472  * of warm entries in syn queue, drop request. It is better than
1473  * clogging syn queue with openreqs with exponentially increasing
1474  * timeout.
1475  */
1476         if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) {
1477                 NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
1478                 goto drop;
1479         }
1480
1481         req = inet_reqsk_alloc(&tcp_request_sock_ops);
1482         if (!req)
1483                 goto drop;
1484
1485 #ifdef CONFIG_TCP_MD5SIG
1486         tcp_rsk(req)->af_specific = &tcp_request_sock_ipv4_ops;
1487 #endif
1488
1489         tcp_clear_options(&tmp_opt);
1490         tmp_opt.mss_clamp = TCP_MSS_DEFAULT;
1491         tmp_opt.user_mss  = tp->rx_opt.user_mss;
1492         tcp_parse_options(skb, &tmp_opt, 0, want_cookie ? NULL : &foc);
1493
1494         if (want_cookie && !tmp_opt.saw_tstamp)
1495                 tcp_clear_options(&tmp_opt);
1496
1497         tmp_opt.tstamp_ok = tmp_opt.saw_tstamp;
1498         tcp_openreq_init(req, &tmp_opt, skb);
1499
1500         ireq = inet_rsk(req);
1501         ireq->loc_addr = daddr;
1502         ireq->rmt_addr = saddr;
1503         ireq->no_srccheck = inet_sk(sk)->transparent;
1504         ireq->opt = tcp_v4_save_options(skb);
1505
1506         if (security_inet_conn_request(sk, skb, req))
1507                 goto drop_and_free;
1508
1509         if (!want_cookie || tmp_opt.tstamp_ok)
1510                 TCP_ECN_create_request(req, skb, sock_net(sk));
1511
1512         if (want_cookie) {
1513                 isn = cookie_v4_init_sequence(sk, skb, &req->mss);
1514                 req->cookie_ts = tmp_opt.tstamp_ok;
1515         } else if (!isn) {
1516                 /* VJ's idea. We save last timestamp seen
1517  * from the destination in peer table, when entering
1518  * state TIME-WAIT, and check against it before
1519  * accepting new connection request.
1520  *
1521  * If "isn" is not zero, this request hit alive
1522  * timewait bucket, so that all the necessary checks
1523  * are made in the function processing timewait state.
1524  */
1525                 if (tmp_opt.saw_tstamp &&
1526                     tcp_death_row.sysctl_tw_recycle &&
1527                     (dst = inet_csk_route_req(sk, &fl4, req)) != NULL &&
1528                     fl4.daddr == saddr) {
1529                         if (!tcp_peer_is_proven(req, dst, true)) {
1530                                 NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSPASSIVEREJECTED);
1531                                 goto drop_and_release;
1532                         }
1533                 }

可以看出在1525行条件判断处,开启TCP_TW_RECYCLE时,会进入函数tcp_peer_is_proven,即判断时间戳的递增性,该函数代码如下:

536 bool tcp_peer_is_proven(struct request_sock *req, struct dst_entry *dst, bool paws_check)
537 {
538         struct tcp_metrics_block *tm;
539         bool ret;
540
541         if (!dst)
542                 return false;
543
544         rcu_read_lock();
545         tm = __tcp_get_metrics_req(req, dst);
546         if (paws_check) {
547                 if (tm &&
548                     (u32)get_seconds() - tm->tcpm_ts_stamp < TCP_PAWS_MSL &&
549                     (s32)(tm->tcpm_ts - req->ts_recent) > TCP_PAWS_WINDOW)
550                         ret = false;
551                 else
552                         ret = true;
553         } else {
554                 if (tm && tcp_metric_get(tm, TCP_METRIC_RTT) && tm->tcpm_ts_stamp)
555                         ret = true;
556                 else
557                         ret = false;
558         }
559         rcu_read_unlock();
560
561         return ret;
562 }
563 EXPORT_SYMBOL_GPL(tcp_peer_is_proven);

在549行 (s32)(tm->tcpm_ts - req->ts_recent) > TCP_PAWS_WINDOW) 即为当前时间小于最近该连接上的时间戳(这个时间戳内核会记录的),然后返回了false,连接建立就会失败。

NAT 与 tcp_tw_recycle

当连接通过NAT机器转换到达开启了TCP_TW_RECYCLE的机器上时,由于NAT机器在做转换时并不会修改协议中的时间戳,因此来自不同机器的请求的时间戳不一定是单调递增的,根据上述原理,所以就会出现连接建立失败的情况。

所以一般机器都是关闭了该选项的,以避免出现连接建立失败的情况。

参考资料

http://www.isi.edu/touch/pubs/infocomm99/infocomm99-web/

https://tools.ietf.org/html/rfc1122#section-4.2.2.13

https://tools.ietf.org/html/rfc6191#page-3

http://www.cnblogs.com/lulu/p/4149312.html