周末跟朋友讨论了一些 TCP 的问题,在查阅《Linux 服务器高性能编程》这本书的时候,发现书上写了这么一句话:

书上说,处于 TIME_WAIT 状态的连接,在收到相同四元组的 SYN 后,会回 RST 报文,对方收到后就会断开连接。

书中作者只是提了这么一句话,没有给予源码或者抓包图的证据。

起初,我看到也觉得这个逻辑也挺符合常理的,但是当我自己去啃了 TCP 源码后,发现并不是这样的。

所以,今天就来讨论下这个问题,「在 TCP 正常挥手过程中,处于 TIME_WAIT 状态的连接,收到相同四元组的 SYN 后会发生什么?」

问题现象如下图,左边是服务端,右边是客户端:

先说结论

在跟大家分析 TCP 源码前,我先跟大家直接说下结论。

针对这个问题,关键是要看 SYN 的「序列号和时间戳」是否合法,因为处于 TIME_WAIT 状态的连接收到 SYN 后,会判断 SYN 的「序列号和时间戳」是否合法,然后根据判断结果的不同做不同的处理。

先跟大家说明下, 什么是「合法」的 SYN?

  • 合法 SYN:客户端的 SYN 的「序列号」比服务端「期望下一个收到的序列号」要大,并且 SYN 的「时间戳」比服务端「最后收到的报文的时间戳」要大。
  • 非法 SYN:客户端的 SYN 的「序列号」比服务端「期望下一个收到的序列号」要小,或者 SYN 的「时间戳」比服务端「最后收到的报文的时间戳」要小。

上面 SYN 合法判断是基于双方都开启了 TCP 时间戳机制的场景,如果双方都没有开启 TCP 时间戳机制,则 SYN 合法判断如下:

  • 合法 SYN:客户端的 SYN 的「序列号」比服务端「期望下一个收到的序列号」要大。
  • 非法 SYN:客户端的 SYN 的「序列号」比服务端「期望下一个收到的序列号」要小。

收到合法 SYN

如果处于 TIME_WAIT 状态的连接收到「合法的 SYN 」后,就会重用此四元组连接,跳过 2MSL 而转变为 SYN_RECV 状态,接着就能进行建立连接过程。

用下图作为例子,双方都启用了 TCP 时间戳机制,TSval 是发送报文时的时间戳:

上图中,在收到第三次挥手的 FIN 报文时,会记录该报文的 TSval (21),用 ts_recent 变量保存。然后会计算下一次期望收到的序列号,本次例子下一次期望收到的序列号就是 301,用 rcv_nxt 变量保存。

处于 TIME_WAIT 状态的连接收到 SYN 后,因为 SYN 的 seq(400) 大于 rcv_nxt(301),并且 SYN 的 TSval(30) 大于 ts_recent(21),所以是一个「合法的 SYN」,于是就会重用此四元组连接,跳过 2MSL 而转变为 SYN_RECV 状态,接着就能进行建立连接过程。

收到非法的 SYN

如果处于 TIME_WAIT 状态的连接收到「非法的 SYN 」后,就会再回复一个第四次挥手的 ACK 报文,客户端收到后,发现并不是自己期望收到确认号(ack num),就回 RST 报文给服务端。

用下图作为例子,双方都启用了 TCP 时间戳机制,TSval 是发送报文时的时间戳:

上图中,在收到第三次挥手的 FIN 报文时,会记录该报文的 TSval (21),用 ts_recent 变量保存。然后会计算下一次期望收到的序列号,本次例子下一次期望收到的序列号就是 301,用 rcv_nxt 变量保存。

处于 TIME_WAIT 状态的连接收到 SYN 后,因为 SYN 的 seq(200) 小于 rcv_nxt(301),所以是一个「非法的 SYN」,就会再回复一个与第四次挥手一样的 ACK 报文,客户端收到后,发现并不是自己期望收到确认号,就回 RST 报文给服务端。

客户端等待一段时间还是没收到 SYN + ACK 后,就会超时重传 SYN 报文,重传次数达到最大值后,就会断开连接。

PS:这里先埋一个疑问,处于 TIME_WAIT 状态的连接,收到 RST 会断开连接吗?

源码分析

下面源码分析是基于 Linux 4.2 版本的内核代码。

Linux 内核在收到 TCP 报文后,会执行 tcp_v4_rcv 函数,在该函数和 TIME_WAIT 状态相关的主要代码如下:

int tcp_v4_rcv(struct sk_buff *skb)
{
  struct sock *sk;
 ...
  //收到报文后,会调用此函数,查找对应的 sock
 sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
          th->dest, sdif, &refcounted);
 if (!sk)
  goto no_tcp_socket;

process:
  //如果连接的状态为 time_wait,会跳转到 do_time_wait
 if (sk->sk_state == TCP_TIME_WAIT)
  goto do_time_wait;

...

do_time_wait:
  ...
  //由tcp_timewait_state_process函数处理在 time_wait 状态收到的报文
 switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {
    // 如果是TCP_TW_SYN,那么允许此 SYN 重建连接
    // 即允许TIM_WAIT状态跃迁到SYN_RECV
    case TCP_TW_SYN: {
      struct sock *sk2 = inet_lookup_listener(....);
      if (sk2) {
          ....
          goto process;
      }
    }
    // 如果是TCP_TW_ACK,那么,返回记忆中的ACK
    case TCP_TW_ACK:
      tcp_v4_timewait_ack(sk, skb);
      break;
    // 如果是TCP_TW_RST直接发送RESET包
    case TCP_TW_RST:
      tcp_v4_send_reset(sk, skb);
      inet_twsk_deschedule_put(inet_twsk(sk));
      goto discard_it;
     // 如果是TCP_TW_SUCCESS则直接丢弃此包,不做任何响应
    case TCP_TW_SUCCESS:;
 }
 goto discard_it;
}

该代码的过程:

  1. 接收到报文后,会调用 __inet_lookup_skb() 函数查找对应的 sock 结构;
  2. 如果连接的状态是 TIME_WAIT,会跳转到 do_time_wait 处理;
  3. 由 tcp_timewait_state_process() 函数来处理收到的报文,处理后根据返回值来做相应的处理。

先跟大家说下,如果收到的 SYN 是合法的,tcp_timewait_state_process() 函数就会返回 TCP_TW_SYN,然后重用此连接。如果收到的 SYN 是非法的,tcp_timewait_state_process() 函数就会返回 TCP_TW_ACK,然后会回上次发过的 ACK。

接下来,看 tcp_timewait_state_process() 函数是如何判断 SYN 包的。

enum tcp_tw_status
tcp_timewait_state_process(struct inet_timewait_sock *tw, struct sk_buff *skb,
      const struct tcphdr *th)
{
 ...
  //paws_reject 为 false,表示没有发生时间戳回绕
  //paws_reject 为 true,表示发生了时间戳回绕
 bool paws_reject = false;

 tmp_opt.saw_tstamp = 0;
  //TCP头中有选项且旧连接开启了时间戳选项
 if (th->doff > (sizeof(*th) >> 2) && tcptw->tw_ts_recent_stamp) { 
  //解析选项
    tcp_parse_options(twsk_net(tw), skb, &tmp_opt, 0, NULL);

  if (tmp_opt.saw_tstamp) {
   ...
      //检查收到的报文的时间戳是否发生了时间戳回绕
   paws_reject = tcp_paws_reject(&tmp_opt, th->rst);
  }
 }

....

  //是SYN包、没有RST、没有ACK、时间戳没有回绕,并且序列号也没有回绕,
 if (th->syn && !th->rst && !th->ack && !paws_reject &&
     (after(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt) ||
      (tmp_opt.saw_tstamp && //新连接开启了时间戳
       (s32)(tcptw->tw_ts_recent - tmp_opt.rcv_tsval) < 0))) { //时间戳没有回绕
    // 初始化序列号
    u32 isn = tcptw->tw_snd_nxt + 65535 + 2; 
    if (isn == 0)
      isn++;
    TCP_SKB_CB(skb)->tcp_tw_isn = isn;
    return TCP_TW_SYN; //允许重用TIME_WAIT四元组重新建立连接
 }

 if (!th->rst) {
    // 如果时间戳回绕,或者报文里包含ack,则将 TIMEWAIT 状态的持续时间重新延长
  if (paws_reject || th->ack)
    inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
        TCP_TIMEWAIT_LEN);

     // 返回TCP_TW_ACK, 发送上一次的 ACK
    return TCP_TW_ACK;
 }
 inet_twsk_put(tw);
 return TCP_TW_SUCCESS;
}

如果双方启用了 TCP 时间戳机制,就会通过 tcp_paws_reject() 函数来判断时间戳是否发生了回绕,也就是「当前收到的报文的时间戳」是否大于「上一次收到的报文的时间戳」:

  • 如果大于,就说明没有发生时间戳绕回,函数返回 false。
  • 如果小于,就说明发生了时间戳回绕,函数返回 true。

从源码可以看到,当收到 SYN 包后,如果该 SYN 包的时间戳没有发生回绕,也就是时间戳是递增的,并且 SYN 包的序列号也没有发生回绕,也就是 SYN 的序列号「大于」下一次期望收到的序列号。就会初始化一个序列号,然后返回 TCP_TW_SYN,接着就重用该连接,也就跳过 2MSL 而转变为 SYN_RECV 状态,接着就能进行建立连接过程。

如果双方都没有启用 TCP 时间戳机制,就只需要判断 SYN 包的序列号有没有发生回绕,如果 SYN 的序列号大于下一次期望收到的序列号,就可以跳过 2MSL,重用该连接。

如果 SYN 包是非法的,就会返回 TCP_TW_ACK,接着就会发送与上一次一样的 ACK 给对方。

在 TIME_WAIT 状态,收到 RST 会断开连接吗?

在前面我留了一个疑问,处于 TIME_WAIT 状态的连接,收到 RST 会断开连接吗?

会不会断开,关键看 net.ipv4.tcp_rfc1337 这个内核参数(默认情况是为 0):

  • 如果这个参数设置为 0, 收到 RST 报文会提前结束 TIME_WAIT 状态,释放连接。
  • 如果这个参数设置为 1, 就会丢掉 RST 报文。

源码处理如下:

enum tcp_tw_status
tcp_timewait_state_process(struct inet_timewait_sock *tw, struct sk_buff *skb,
      const struct tcphdr *th)
{
....
  //rst报文的时间戳没有发生回绕
 if (!paws_reject &&
     (TCP_SKB_CB(skb)->seq == tcptw->tw_rcv_nxt &&
      (TCP_SKB_CB(skb)->seq == TCP_SKB_CB(skb)->end_seq || th->rst))) {

      //处理rst报文
      if (th->rst) {
        //不开启这个选项,当收到 RST 时会立即回收tw,但这样做是有风险的
        if (twsk_net(tw)->ipv4.sysctl_tcp_rfc1337 == 0) {
          kill:
          //删除tw定时器,并释放tw
          inet_twsk_deschedule_put(tw);
          return TCP_TW_SUCCESS;
        }
      } else {
        //将 TIMEWAIT 状态的持续时间重新延长
        inet_twsk_reschedule(tw, TCP_TIMEWAIT_LEN);
      }

      ...
      return TCP_TW_SUCCESS;
    }
}

TIME_WAIT 状态收到 RST 报文而释放连接,这样等于跳过 2MSL 时间,这么做还是有风险。

sysctl_tcp_rfc1337 这个参数是在 rfc 1337 文档提出来的,目的是避免因为 TIME_WAIT 状态收到 RST 报文而跳过 2MSL 的时间,文档里也给出跳过 2MSL 时间会有什么潜在问题。

TIME_WAIT 状态之所以要持续 2MSL 时间,主要有两个目的:

  • 防止历史连接中的数据,被后面相同四元组的连接错误的接收;
  • 保证「被动关闭连接」的一方,能被正确的关闭;

详细的为什么要设计 TIME_WAIT 状态,我在这篇有详细说明:如果 TIME_WAIT 状态持续时间过短或者没有,会有什么问题?

虽然 TIME_WAIT 状态持续的时间是有一点长,显得很不友好,但是它被设计来就是用来避免发生乱七八糟的事情。

《UNIX网络编程》一书中却说道:TIME_WAIT 是我们的朋友,它是有助于我们的,不要试图避免这个状态,而是应该弄清楚它。

所以,我个人觉得将 net.ipv4.tcp_rfc1337 设置为 1 会比较安全。

总结

在 TCP 正常挥手过程中,处于 TIME_WAIT 状态的连接,收到相同四元组的 SYN 后会发生什么?

如果双方开启了时间戳机制:

  • 如果客户端的 SYN 的「序列号」比服务端「期望下一个收到的序列号」要大,并且SYN 的「时间戳」比服务端「最后收到的报文的时间戳」要大。那么就会重用该四元组连接,跳过 2MSL 而转变为 SYN_RECV 状态,接着就能进行建立连接过程。
  • 如果客户端的 SYN 的「序列号」比服务端「期望下一个收到的序列号」要小,或者SYN 的「时间戳」比服务端「最后收到的报文的时间戳」要小。那么就会再回复一个第四次挥手的 ACK 报文,客户端收到后,发现并不是自己期望收到确认号,就回 RST 报文给服务端。

在 TIME_WAIT 状态,收到 RST 会断开连接吗?

  • 如果 net.ipv4.tcp_rfc1337 参数为 0,则提前结束 TIME_WAIT 状态,释放连接。
  • 如果 net.ipv4.tcp_rfc1337 参数为 1,则会丢掉该 RST 报文。

完!

在 TIME_WAIT 状态的 TCP 连接,收到 SYN 后会发生什么?的更多相关文章

  1. 当TIME_WAIT状态的TCP正常挥手,收到SYN后…

    摘要:今天就来讨论下这个问题,在TCP正常挥手过程中,处于TIME_WAIT状态的连接,收到相同四元组的SYN后会发生什么? 本文分享自华为云社区<在TIME_WAIT状态的TCP连接,收到SY ...

  2. 好一个Time_Wait状态(TCP/IP)

    首先简单介绍一下Time_Wait是个什么鬼: 在TCP/IP协议中,我们都知道有三次握手四次挥手的过程,先来一个简单的图: 各个状态和基本的过程想必了解过TCP/IP协议的人都清楚,本次介绍的主题只 ...

  3. TCP协议(二)——TIME_WAIT状态

    当TCP主动关闭套接字时,采用四步握手机制来彻底关闭连接.如图: 客户端主动关闭连接,发送FIN段到服务端.TCP状态由ESTABLISHED(连接状态)转为FIN_WAIT1(表示,发送的FIN需要 ...

  4. 经典!服务端 TCP 连接的 TIME_WAIT 过多问题的分析与解决

    开源Linux 专注分享开源技术知识 本文给出一个 TIME_WAIT 状态的 TCP 连接过多的问题的解决思路,非常典型,大家可以好好看看,以后遇到这个问题就不会束手无策了. 问题描述 模拟高并发的 ...

  5. Java面试-TCP连接及其优化

    作为一个后端程序员,网络连接这块是一个绕不过的砍,当你在做服务器优化的时候,网络优化也是其中一环,那么作为网络连接中最基础的部分-TCP连接你了解吗?今天我们来仔细看看这个部分. TCP建立连接-三次 ...

  6. linux系统telnet端口不通能收到SYN但不回SYN+ACK响应问题排查(转载)

    linux系统telnet端口不通能收到SYN但不回SYN+ACK响应问题排查 一:背景:一台机器从公司办公网登录不上且所有tcp端口都telnet不通,但是通过同机房同的其它机器却可以正常访问到出问 ...

  7. 服务端挂了,客户端的 TCP 连接还在吗?

    作者:小林coding 计算机八股文网站:https://xiaolincoding.com 大家好,我是小林. 如果「服务端挂掉」指的是「服务端进程崩溃」,服务端的进程在发生崩溃的时候,内核会发送 ...

  8. netstat -an查看到大量的TIME_WAIT状态的解决办法

    netstat下time_wait状态的tcp连接: 1.这是一种处于连接完全关闭状态前的状态: 2.通常要等上4分钟(windows server)的时间才能完全关闭: 3.这种状态下的tcp连接占 ...

  9. TCP连接状态详解及TIME_WAIT过多的解决方法

    上图对排除和定位网络或系统故障时大有帮助,但是怎样牢牢地将这张图刻在脑中呢?那么你就一定要对这张图的每一个状态,及转换的过程有深刻地认识,不能只停留在一知半解之中.下面对这张图的11种状态详细解释一下 ...

随机推荐

  1. 通过Javascript实现把数组里的内容以表格方式呈现到页面从

    一.把数组里的内容呈现到页面从,以表格方式 <!doctype html> <html> <head> <meta charset="utf-8&q ...

  2. 【记录一个问题】linux + opencv + gpu视频解码,好不容易编译通过,运行又coredump了

    1.首先编译了opencv + cuda   编译选项中使用了以下关于cuvid库的内容: //"nvcuvid" libraryCUDA_nvcuvid_LIBRARY:FILE ...

  3. NIO-java.nio.ByteBuffer中flip、rewind、clear方法的区别

    Java NIO中的Buffer用于和NIO通道进行交互.如你所知,数据是从通道读入缓冲区,从缓冲区写入到通道中的. 缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存.这块内存被包装成NIO ...

  4. Windows和Linux关闭占用端口

    关闭端口的方式有很多种,但是常用的就是这种比较来的快一点 如果通过以下方式解决不了,可以通过关闭服务来解决 Windows 1.查看端口占用的进程 netstat -ano | findstr 800 ...

  5. vue-cli创建的项目打包成app引入字体图标的问题

    将项目在手机端调试时,发现引入的阿里图标显示不出来,需要在引入的iconfont.css文件夹里给url加上https

  6. 操作系统的发展史(并发与并行)<异步与同步>《进程与程序》[非堵塞与堵塞]

    目录 一:一:手工操作 -- 穿孔卡片 1.简介 二:手工操作方式两个特点: 三:批处理 -- 磁带存储 1.联机批处理系统 2.脱机批处理系统 3.多道程序系统 4.多道批处理系统 四:总结发展史 ...

  7. Android 资源溢出崩溃轻松解

    作者:字节跳动终端技术-李权飞 资源溢出是什么? 毫无疑问,应用的运行需要占用系统的资源.其中最为人所熟知的资源是内存,内存溢出便是耳熟能详的OOM. 常见的简单OOM一般可以通过堆栈来解决,如Jav ...

  8. 在EntityFrameworkCore中记录EF修改日志,保存,修改字段的原始值,当前值,表名等信息

    突发奇想,想把业务修改的所有字段原始值和修改后的值,做一个记录,然后发现使用EF可以非常简单的实现这个功能 覆盖父类中的 SaveShanges() 方法 public new int SaveCha ...

  9. js源码-自定义数组的pop和shift方法

    本文将自定义_pop和_shift来模拟数组的pop和shift方法 _pop: /* *js中数组的pop方法:删除数组的最后一个元素,把数组的长度减1,并且返回删除的这个元素:如果数组为空,则po ...

  10. [NOI2020]命运

    显然直接计数是不好计的,只能从 \(dp\) 这个角度来下手. 首先用最原始最直接的方法,直接在 \(dp\) 的过程中满足题目的要求. 既然问题给在一棵树上,那么必然和树脱不了关系,因此我们应该从树 ...