tcp_tw_recycle引起的TCP握手失败

article/2025/10/1 18:42:02

背景

测试环境的一台Nginx服务器,最近一直被前端同事吐槽网络有问题,经常出现访问HTTP请求时超时,哪怕是静态文件也经常超时。

刚开始以为是公司网络抽风了,也就没放在心上,但持续了一个星期,而且复现率很高,这才反应过来应该不是网络的锅。于是在请求客户端与Nginx服务器上均作了抓包。

本地客户端抓包结果如下图1,请求Nginx服务器TCP握手时超时。结果似乎很明朗,客户端TCP握手的SYN请求丢包导致多次重试,直到重试超时而TCP握手失败。看上去似乎就是网络问题,但这复现率也太高了,于是在服务器上也做了一次抓包。

图2为Nginx服务器抓包图,显示服务器接收到了TCP握手请求,但是未做应答,直到客户端重试超时结束。很明显,并不是网络丢包而导致的TCP握手失败,而是某种机制导致的服务器直接抛弃了客户端的请求报文。

在这里插入图片描述

复现

准备

准备两个Client机器(192.168.50.150、192.168.50.160)以及一个Server机器(118.24.117.115),并且两个Client机器均处于同一个NAT网关下(116.239.x.x)。

在这里插入图片描述

启用两个Client机器TIME-WAIT连接快速回收,编辑文件/etc/sysctl.conf,添加或修改以下参数:

net.ipv4.tcp_tw_recycle = 1

然后执行命令使sysctl.conf的参数生效:

[root@VM_0_10_centos ~]# /sbin/sysctl -p
[root@VM_0_10_centos ~]# cat /proc/sys/net/ipv4/tcp_tw_recycle 
1

复现步骤

  1. Client1机器请求Server,Client1抓包和Server抓包如下两张图,TCP握手成功:

在这里插入图片描述

在这里插入图片描述

  1. Client2机器请求Server,Client2抓包和Server抓包如下两张图,TCP握手成功:

在这里插入图片描述

在这里插入图片描述

  1. Client1机器再次请求Server,Client1抓包和Server抓包如下两张图,TCP握手失败,复现成功:

在这里插入图片描述

在这里插入图片描述

通过netstat -s命令查看网络统计时,可以发现每次复现出TCP握手失败后,如下的一行统计值都会对应增长。

[root@VM_0_10_centos ~]# netstat -s | grep timestamp2143 packets rejects in established connections because of timestamp

原理

PAWS机制

Per-host PAWS机制

PAWS uses the same TCP Timestamps option as the RTTM mechanism described earlier, and assumes that every received TCP segment (including data and ACK segments) contains a timestamp SEG.TSval whose values are monotone non-decreasing in time. The basic idea is that a segment can be discarded as an old duplicate if it is received with a timestamp SEG.TSval less than some timestamp recently received on this connection.

PAWS机制是基于客户端IP而非客户端IP+端口号的,当服务端开启了net.ipv4.tcp_tw_recycle后,服务端会对SYN报文做时间戳检查。每当快速回收TIME_WAIT连接后,会在60秒缓存该客户端IP的TSval时间戳。在这60秒内如果同一个IP再发起SYN请求时,会校验新请求的TSval是否大于缓存的TSval,保证同一个请求方IP的TSval时间戳是递增的。而对于非递增的SYN请求则直接丢弃处理。

TSval并非真正的时间戳,而是由时间戳依据一定算法算出来的一个值,与时间戳有同等的特性,即随时间单调递增;

如上复现操作时的图,由于Client1和Client2处于同一个NAT网关下,对于Server来说,Client1和Client2的IP相同均为NAT网关的IP。

  1. 当Client1请求Server时,IP(116.239.x.x)的时间戳值TSval为1853021;
  2. 当Client2请求Server时,IP(116.239.x.x)的时间戳值TSval为1927141;
  3. 当Client1再次请求Server时,IP(116.239.x.x)的时间戳值TSval为1864558;

当步骤3请求时,对于Server来说,IP(116.239.x.x)的TSval时间戳小于上次请求的时间戳,因此该SYN请求直接被丢弃了,导致TCP握手失败。

Linux内核的实现

Linux内核源码中,对于PAWS机制的实现很简单,如果客户端的TCP报文中启用了timestamp option,且服务端启用了net.ipv4.tcp_tw_recycle,则会触发PAWS机制检查。

tcp_ipv4.c

int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
.../* VJ's idea. We save last timestamp seen* from the destination in peer table, when entering* state TIME-WAIT, and check against it before* accepting new connection request.** If "isn" is not zero, this request hit alive* timewait bucket, so that all the necessary checks* are made in the function processing timewait state.*/if (tmp_opt.saw_tstamp &&   // TCP报文中启用了timestamp optiontcp_death_row.sysctl_tw_recycle &&  // 开启了net.ipv4.tcp_tw_recycle(dst = inet_csk_route_req(sk, &fl4, req)) != NULL &&fl4.daddr == saddr) {   // 仅判断源IP相同,不区分端口号if (!tcp_peer_is_proven(req, dst, true)) {NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSPASSIVEREJECTED);goto drop_and_release;}}...
}

tcp_metrics.c

bool tcp_peer_is_proven(struct request_sock *req, struct dst_entry *dst, bool paws_check)
{struct tcp_metrics_block *tm;bool ret;if (!dst)return false;rcu_read_lock();tm = __tcp_get_metrics_req(req, dst);if (paws_check) {if (tm &&// peer 信息保存的时间离现在在60秒(TCP_PAWS_MSL)之内(u32)get_seconds() - tm->tcpm_ts_stamp < TCP_PAWS_MSL &&// peer 信息中保存的timestamp 比当前收到的SYN报文中的timestamp大1(TCP_PAWS_WINDOW)(s32)(tm->tcpm_ts - req->ts_recent) > TCP_PAWS_WINDOW)ret = false;elseret = true;} else {if (tm && tcp_metric_get(tm, TCP_METRIC_RTT) && tm->tcpm_ts_stamp)ret = true;elseret = false;}rcu_read_unlock();return ret;
}

解决

关闭TCP的TIME_WAIT快速回收功能即可:编辑文件/etc/sysctl.conf,添加或修改以下参数:

net.ipv4.tcp_tw_recycle = 0

然后执行命令使sysctl.conf的参数生效:

[root@VM_0_10_centos ~]# /sbin/sysctl -p
[root@VM_0_10_centos ~]# cat /proc/sys/net/ipv4/tcp_tw_recycle 
0

疑问

查看Linux内核源码后发现,触发PAWS机制检查的前提条件之一是客户端的TCP请求携带了timestamp option,如下图1。但是当使用Windows操作系统时,客户端默认关闭了timestamp option,通过抓包也显示TCP请求中未携带timestamp option,却依然触发了PAWS机制,如下图2。

在这里插入图片描述

在这里插入图片描述

参考

  1. Linux内核协议栈丢弃SYN报文的主要场景剖析
  2. Tcp tw recycle打开导致60s内不能两个用户同时登录
  3. 开启tcp_tw_recycle内核参数在NAT环境会丢包

http://chatgpt.dhexx.cn/article/4VuBwH1c.shtml

相关文章

TCP握手过程(正解版)

参考文章 Why do we need a 3-way handshake? Why not just 2-way https://blog.csdn.net/qq_36903042/article/details/102656641 https://blog.csdn.net/qq_36903042/article/details/102513465 大部分网络博客的错误解读 首先需要声明的是&#xff0c; 百度搜索到的大…

网络协议 (五) TCP握手建立连接

一、握手策略 为了可以准确的将数据准确无误地送达目标主机&#xff0c;所有基于 TCP 实现的协议&#xff0c;都需要先完成 TCP 协议的三次握手策略。 1. 首先我们需要了解一下图中提到的几个标志符&#xff1a; 1.序号seq seq 是TCP通信过程中&#xff0c;某一个传输方向上字…

TCP 握手没成功怎么办?

大家好&#xff0c;我是小林。 之前收到个读者的问题&#xff0c;对于 TCP 三次握手和四次挥手的一些疑问&#xff1a; 第一次握手&#xff0c;如果客户端发送的SYN一直都传不到被服务器&#xff0c;那么客户端是一直重发SYN到永久吗&#xff1f;客户端停止重发SYN的时机是什么…

深入理解TCP三次握手

一、TCP 包头格式 首先&#xff0c;TCP报文是TCP层传输的数据单元&#xff0c;也称为报文段&#xff0c;下面就是TCP包头格式&#xff1a; 接下来我们来看看每个字段的含义&#xff1a; 源端口和端口字号&#xff1a; TCP源端口&#xff1a;源计算机上应用程序端的端口号&…

TCP的三次握手及四次挥手详解

三次握手 三次握手过程&#xff1a; &#xff08;1&#xff09;第一次握手&#xff1a;Client将标志位SYN置为1&#xff08;表示要发起一个连接&#xff09;&#xff0c;随机产生一个值seqJ&#xff0c;并将该数据包发送给Server&#xff0c;Client进入SYN_SENT状态&#xff0c…

C语言中main函数参数使用

在C99标准中定义main函数两种正确的写法 int main(void); int main(int argc, char* argv[]);常见的不标准写法 void main() main()这里主要说明带参数的main函数如何使用 int main(int argc, char* argv[]) {int i;for (i0; i<argc; i)printf("%d: %s\r\n", i…

C语言main函数参数、返回值

C语言main函数返回值&#xff1a; main函数的返回值&#xff0c;用于说明程序的退出状态。如果返回0&#xff0c;则代表程序正常退出&#xff1b;返回其他数字的含义则由系统决定&#xff0c;通常&#xff0c;返回非零代表程序异常退出&#xff0c;即使程序运行结果正确也仍需修…

main 函数的参数说明

C/C语言中的main函数&#xff0c;经常带有参数argc&#xff0c;argv&#xff0c;如下&#xff1a; int main(int argc, char** argv)int main(int argc, char* argv[])这两个参数的作用是什么呢&#xff1f;argc 是指命令行输入参数的个数&#xff0c;argv存储了所有的命令行参…

C++ main函数及main函数的参数

C main函数及main函数的参数 1、main函数的几种形式 int main() int main(int argc) int main(int argc,char** argv)//int main(int argc,char* argv[])2、argc表示命令行参数的个数、argv表示命令行参数的值 &#xff08;1&#xff09;写个小代码&#xff0c;用命令行运行…

带参数的main函数

支持C语言的系统允许main函数有两个参数 int main(int argc,char *argv[]){//argc表示从命令行传入的参数的个数&#xff1b;//argv表示从命令行传入的字符串数组&#xff1b; } 回显命令行参数 #include<stdio.h> int main(int argc,char *argv[]){int i;for(i0;i<…

main主函数参数解析

默认的main函数参数 int main(int argc, char *argv[]) {// 主函数的代码逻辑return 0; }其中&#xff0c;int 是主函数的返回值类型&#xff0c;主函数执行完后会返回一个整数值给操作系统&#xff0c;通常返回值为 0 表示程序正常结束&#xff0c;非 0 的返回值表示程序运行…

C语言main函数参数

常见的C语言的main函数都是不带参数的。因此main 后的括号都是空括号。实际上&#xff0c;main函数可以带参数&#xff0c;这个参数可以认为是main函数的形式参数。C语言规定main函数的参数只能有两个&#xff0c;习惯上这两个参数写为argc和argv。因此&#xff0c;main函数的函…

【C语言】main函数的参数

我们先看看主函数main的参数列表 #include <stdio.h>int main(int argc, char *argv[]) {return 0; }argc是一个整型变量&#xff0c;存储的是主函数的参数个数argv[]是一个字符型指针数组&#xff0c;其中存储的是主函数的参数字符串&#xff0c;是一个参数列表 注意:…

C++中main函数的参数

C中main函数的参数 问题 对于一个C/CPP文件来说&#xff0c;一般都有main函数&#xff0c;一个程序的执行一定会是先从main函数这个入口开始&#xff0c;一个最基本的程序框架大概长这样: #include <stdio.h> int main() {return 0; }但是&#xff0c;我们有时候看到的…

C++ main函数的参数

int main(int argc, char* argv[]) 那main函数的argc和argv参数是什么意思呢&#xff1f; 这两个参数主要是用来保存程序运行时传递给main函数的命令行参数的。 argc&#xff1a;是argument count 的缩写&#xff0c;保存运行时传递给main函数的参数个数。 argv&#xff1a;是…

main函数的参数的含义

转载自&#xff1a;点击链接 链接2 加qq1126137994 微信&#xff1a;liu1126137994 一起学习更多技术&#xff01;&#xff01;&#xff01; 最近学习服务器网络编程&#xff0c;遇到了一个问题&#xff0c;main函数的参数&#xff0c;特意整理资料记录之&#xff01;&#…

C++main函数及参数

转载自&#xff1a;https://www.cnblogs.com/carsonzhu/p/5276317.html C main()函数及其参数 1、首先&#xff0c;想想C/C在main函数之前和之后会做些什么&#xff1f; 我们看看底层的汇编代码&#xff1a; __start: :      init stack;      init heap;     …

带参数的的main函数

在main()函数中允许带2个参数&#xff0c;一个为整型argc,另一个是指向字符型的指针数组argv[]。格式&#xff1a; int main(int argc,char *argv[])   其中整型argc表示命令行中字符串的个数&#xff0c;指针数组argv[]指向命令行中的各个字符串。这两个参数可以用任何合法…

C语言main函数参数详解

main函数参数 一共有三个&#xff1a; 1.int argc 整型变量 2.char *argv[] 字符指针的数组&#xff0c;通俗一点就是字符串数组&#xff0c;每个元素都是字符串 3.char *envp[] 字符串数组 这三个东西再怎么神秘&#xff0c;也只是函数参数&#xff0c;只不过是main函数的参数…

main函数的参数

main函数可以不带参数,也可以带参数&#xff0c;这个参数可以认为是 main函数的形式参数。C语言规定main函数的参数只能有两个&#xff0c;习惯上这两个参数写为argc和argv。因此&#xff0c;main函数的函数头可写为&#xff1a; main (argc,argv) C语言还规定argc(第一个形参…