linux tcp 在timewait 状态下的报文处理
最近处理一个问题,我们nginx服务器作为透明代理,将核心网过来的用户上网请求代理到我们的cache服务器,如果cache服务器没有命中内容,则需要我们
作为客户端往源站请求内容,但用户对此一无所知,也就是我们使用透明代理的模式来给用户提供上网服务。
问题出在:我们作为客户端,往服务器端请求数据。服务器端主动断链之后,我们使用相同的ip和端口去连接服务器端,发现syn 没有得到响应。
从图中TCP Port numbers reused 开始这行可以看出:
106.332208 我们服务器在收到源站的主动断链请求
106.371754 我们服务器发送了针对源站主动fin的ack。
107.597531 我们服务器收到用户的一个GET 请求,
107.598388 我们服务器调用close(socket),触发内核发送了fin请求给源站。
107.605880 我们服务器收到源站返回的针对我们fin的ack,在此,四次挥手结束。那么主动断链的源站,肯定处于time_wait状态。
107.636754 我们服务器收到用户的一个ack,这个因为我们服务器使用用户的ip和端口跟源站交互,所以ip和端口是一样的,所以只能从Seq,Ack,或者mac地址来区分链路。
倒数的四个报文:
109.597985 我们服务器使用新的socket,但是ip和端口跟之前的链路一样,往源站进行connect,触发内核发送syn请求,
110.600579 我们服务器的第一个syn未收到回复,重发该请求。1s超时
112.604765 我们服务器退避发送syn请求。2s超时
116.613191 我们服务器在退避之后,4s超时,达到tcp_syn_retries 设置的2次上限,无奈给用户回复502.
报文分析完毕,我们在排除丢包的情况下,想想源站为什么会对我们的syn无动于衷。
下面都是假设源站是linux 3.10下的实现。
由于源站是主动断链,在回复给我们服务器的fin的ack之后,进入time_wait状态。
int tcp_v4_rcv(struct sk_buff *skb)
{
。。。
sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
if (!sk)----这里搜出来的sk,其实是inet_timewait_sock
goto no_tcp_socket; process:
if (sk->sk_state == TCP_TIME_WAIT)----------大状态是time_wait,大状态下又分为两个子状态,如fin_wait2,time_wait
goto do_time_wait;
。。。
do_time_wait:
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
inet_twsk_put(inet_twsk(sk));
goto discard_it;
} if (skb->len < (th->doff << 2)) {
inet_twsk_put(inet_twsk(sk));
goto bad_packet;
}
if (tcp_checksum_complete(skb)) {
inet_twsk_put(inet_twsk(sk));
goto csum_error;
}
switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {----返回四种结果
case TCP_TW_SYN: {----------合理的syn,处理建联请求
struct sock *sk2 = inet_lookup_listener(dev_net(skb->dev),---------查找监听socket
&tcp_hashinfo,
iph->saddr, th->source,
iph->daddr, th->dest,
inet_iif(skb));
if (sk2) {---找到对应listen的socket,则继续处理,注意这个sk已经是listen的sk了。
inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);
inet_twsk_put(inet_twsk(sk));
sk = sk2;
goto process;
}
/* Fall through to ACK */---------没找到listen的socket的话,则没有break,会进入下面的TCP_TW_ACK,回复ack并丢弃skb
}
case TCP_TW_ACK:---------回ack
tcp_v4_timewait_ack(sk, skb);
break;
case TCP_TW_RST:---------关闭链路
tcp_v4_send_reset(sk, skb);---发送rst包给对端,
inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);
inet_twsk_put(inet_twsk(sk));
goto discard_it;
case TCP_TW_SUCCESS:;----虽然叫success,但是什么都不做,空语句,最终会走到discrad_it
}
goto discard_it;
}
}
为了减少一点内存占用,在tcp_time_wait 函数中,将处于timewait状态的sock 替换为了 inet_timewait_sock 。
crash> p sizeof(struct tcp_sock)
$5 = 1968
crash> p sizeof(struct inet_timewait_sock)
$6 = 152
也就是处于time_wait状态的socket比处于正常状态的socket少占用了1.8k内存,对于很多服务器来说,timewait状态下的socket比较多,算起来也很可观了,所以,linux又设计了一个
tcp_max_tw_buckets 来限制处于time_wait的数量。
这个也是 tcp_timewait_state_process(inet_twsk(sk), skb, th) 中能够将sock直接转换为 inet_timewait_sock 的原因。
从流程看,需要分析 tcp_timewait_state_process 的处理:
enum tcp_tw_status
tcp_timewait_state_process(struct inet_timewait_sock *tw, struct sk_buff *skb,
const struct tcphdr *th)
{
struct tcp_options_received tmp_opt;
struct tcp_timewait_sock *tcptw = tcp_twsk((struct sock *)tw);
bool paws_reject = false; tmp_opt.saw_tstamp = 0;
if (th->doff > (sizeof(*th) >> 2) && tcptw->tw_ts_recent_stamp) {
tcp_parse_options(skb, &tmp_opt, 0, NULL); if (tmp_opt.saw_tstamp) {
tmp_opt.rcv_tsecr -= tcptw->tw_ts_offset;
tmp_opt.ts_recent = tcptw->tw_ts_recent;
tmp_opt.ts_recent_stamp = tcptw->tw_ts_recent_stamp;
paws_reject = tcp_paws_reject(&tmp_opt, th->rst);
}
}--------------这个是时间戳的检查,我们自己作为请求方但是没有开启时间戳,所以paws_reject为0,saw_tstamp为0. if (tw->tw_substate == TCP_FIN_WAIT2) {-----根据挥手流程,处于fin_wait2状态的socket会在收到fin之后迁入time_wait状态,这个是指tw_substate也是time_wait状态
/* Just repeat all the checks of tcp_rcv_state_process() */ /* Out of window, send ACK */
if (paws_reject ||--------如注释,超过接收包的tcp窗口。则走oow流程
!tcp_in_window(TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq,
tcptw->tw_rcv_nxt,
tcptw->tw_rcv_nxt + tcptw->tw_rcv_wnd))
return tcp_timewait_check_oow_rate_limit(
tw, skb, LINUX_MIB_TCPACKSKIPPEDFINWAIT2); if (th->rst)---收到rst包,直接kill,但是要注意的是,kill返回的其实是 TCP_TW_SUCCESS,也就是啥都不干。
goto kill; if (th->syn && !before(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt))---在fin_wait2状态,收到syn,并且seq小于我们需要接收的nxt,则rst掉,认为是过期的syn
return TCP_TW_RST; /* Dup ACK? */
if (!th->ack ||---没有ack标志,则丢弃,说明走到这的肯定都带ack标志,因为就算是fin,ack标志也是设置的。
!after(TCP_SKB_CB(skb)->end_seq, tcptw->tw_rcv_nxt) ||-----有ack标志,但是end_seq在窗口左边,也就是oow,有可能是重复ack,丢弃
TCP_SKB_CB(skb)->end_seq == TCP_SKB_CB(skb)->seq) {---是纯ack,我们是因为收到fin-ack才进入的fin-wait2,现在又来个纯ack,不是fin,也不是syn,丢弃
inet_twsk_put(tw);
return TCP_TW_SUCCESS;
} /* New data or FIN. If new data arrive after half-duplex close,
* reset.
*/
if (!th->fin ||---不带fin标志,直接rst掉
TCP_SKB_CB(skb)->end_seq != tcptw->tw_rcv_nxt + 1)---是fin包,收到的seq有数据,rst掉,看这意思,不能fin带数据。
return TCP_TW_RST; /* FIN arrived, enter true time-wait state. */
tw->tw_substate = TCP_TIME_WAIT;----------到这的,肯定是有fin标志的,否则前面就返回了,fin-wait2收到fin,迁入time_wait状态,此时子状态也是time_wait了
tcptw->tw_rcv_nxt = TCP_SKB_CB(skb)->end_seq;
if (tmp_opt.saw_tstamp) {
tcptw->tw_ts_recent_stamp = get_seconds();
tcptw->tw_ts_recent = tmp_opt.rcv_tsval;
} if (tcp_death_row.sysctl_tw_recycle &&-----开启了tw_recyle的情况下,
tcptw->tw_ts_recent_stamp &&----------开启了时间戳的情况下下
tcp_tw_remember_stamp(tw))
inet_twsk_schedule(tw, &tcp_death_row, tw->tw_timeout,---设置超时为tw_timeout,这个跟链路相关,在tcp_time_wait 中设置为3.5*RTO。
TCP_TIMEWAIT_LEN);
else
inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,----没有设置时间戳和tw_recyle,则默认的60s,这个值是写死的,尼玛也不让改,只能编译内核
TCP_TIMEWAIT_LEN);
return TCP_TW_ACK;
}-------------如果子状态是fin-wait2,则在这个里面处理 /*
* Now real TIME-WAIT state.---------------------本文syn发送的时候,服务器应该处于这个状态,下面就是服务器收到本syn该执行的代码
*
* RFC 1122:
* "When a connection is [...] on TIME-WAIT state [...]
* [a TCP] MAY accept a new SYN from the remote TCP to
* reopen the connection directly, if it:-----------------在timewait状态下重新open的条件:
*
* (1) assigns its initial sequence number for the new----初始seq比之前老链路ack的序号大
* connection to be larger than the largest sequence
* number it used on the previous connection incarnation,
* and
*
* (2) returns to TIME-WAIT state if the SYN turns out
* to be an old duplicate".
*/ if (!paws_reject &&------------防回绕校验失败
(TCP_SKB_CB(skb)->seq == tcptw->tw_rcv_nxt &&-------当前需要和预期的序号相同且纯fin或者纯rst,
(TCP_SKB_CB(skb)->seq == TCP_SKB_CB(skb)->end_seq || th->rst))) {----rst标志被置位
/* In window segment, it may be only reset or bare ack. */ if (th->rst) {------我们已经处于timewait状态,收到rst,
/* This is TIME_WAIT assassination, in two flavors.
* Oh well... nobody has a sufficient solution to this
* protocol bug yet.
*/
if (sysctl_tcp_rfc1337 == 0) {
kill:
inet_twsk_deschedule(tw, &tcp_death_row);
inet_twsk_put(tw);
return TCP_TW_SUCCESS;--------丢弃这个包
}
}
inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
TCP_TIMEWAIT_LEN); if (tmp_opt.saw_tstamp) {----有时间戳选项的话,更新时间戳
tcptw->tw_ts_recent = tmp_opt.rcv_tsval;
tcptw->tw_ts_recent_stamp = get_seconds();
} inet_twsk_put(tw);
return TCP_TW_SUCCESS;--------丢弃这个包
}--------------显然,我们的syn不满足这个if /* Out of window segment. All the segments are ACKed immediately. The only exception is new SYN. We accept it, if it is
not old duplicate and we are not in danger to be killed
by delayed old duplicates. RFC check is that it has
newer sequence number works at rates <40Mbit/sec.
However, if paws works, it is reliable AND even more,
we even may relax silly seq space cutoff. RED-PEN: we violate main RFC requirement, if this SYN will appear
old duplicate (i.e. we receive RST in reply to SYN-ACK),
we must return socket to time-wait state. It is not good,
but not fatal yet.
*/ if (th->syn && !th->rst && !th->ack && !paws_reject &&-------我们的syn包不含rst标志,也没有ack标志,但没有开启时间戳选项,所以paws_reject为0.满足条件
(after(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt) ||-------我们的syn的序号是2972897916,而老的链路的tw_rcv_nxt为2674663925,满足条件,按道理就&&条件满足
(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;
} if (paws_reject)
NET_INC_STATS_BH(twsk_net(tw), LINUX_MIB_PAWSESTABREJECTED); if (!th->rst) {-----其他情况处理,如不是有效的syn,比如序列号在window之前,ack包,但oow,
/* In this case we must reset the TIMEWAIT timer.
*
* If it is ACKless SYN it may be both old duplicate
* and new good SYN with random sequence number <rcv_nxt.
* Do not reschedule in the last case.
*/
if (paws_reject || th->ack)
inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
TCP_TIMEWAIT_LEN); return tcp_timewait_check_oow_rate_limit(
tw, skb, LINUX_MIB_TCPACKSKIPPEDTIMEWAIT);
}
inet_twsk_put(tw);
return TCP_TW_SUCCESS;
}
为了防止回绕,一般我们通过开启 /proc/sys/net/ipv4/tcp_timestamps 来防止回绕,也就是PAWS(Protect Against Wrapped Sequence numbers) 。
在本案例中,我们发送的syn,按道理是符合条件的,对方为啥一点反应都没有呢?为了弄清楚这个问题,我们发了一堆命令给源站,源站表示看不懂,
后来才知道,因为他们是windows系统来提供网站服务的,因此不能继续分析了。当然也不是没有任何收获,毕竟对于大多数linux服务器的实现流程更
清楚了,从代码看,如果是linux服务器,就算没有建联成功,好歹会回复一个ack,而不是像目前这样啥都不回,导致请求端重传并超时。
状态问题:
tcp 0 0 10.47.242.207:8000 10.47.242.118:7000 FIN_WAIT2 7344/tcp_server.o 12: CFF22F0A:1F40 76F22F0A:1B58 05 00000000:00000000 00:00000000 00000000 0 0 5271486 1 ffff940408230f80 20 4 30 10 -1
根据/proc/net/tcp中的显示,当状态5,也就是 TCP_FIN_WAIT2,因为:
static int tcp4_seq_show(struct seq_file *seq, void *v)
{
...
switch (st->state) {
case TCP_SEQ_STATE_LISTENING:
case TCP_SEQ_STATE_ESTABLISHED:
if (sk->sk_state == TCP_TIME_WAIT)------------当状态为time-wait的时候,会显示子状态
get_timewait4_sock(v, seq, st->num, &len);
else
get_tcp4_sock(v, seq, st->num, &len);
break;
case TCP_SEQ_STATE_OPENREQ:
get_openreq4(st->syn_wait_sk, v, seq, st->num, st->uid, &len);
break;
}
...}
对参数理解的收获:
net.ipv4.tcp_tw_recycle = 0 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭,开启时,回收的时间为3.5*RTO。
net.ipv4.tcp_fin_timeout = 60 表示如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间,如果发送fin的一端是使用shutdown方式来关闭写的一端,
则这个状态可能会维持很长很长,而不是这个60s。
我通过写的简单的tcp的一个简单例子来模拟源站,发现了只要没有将服务器端缓冲区的数据recv干净,调用close的话,会发rst,recv干净之后再调用close的话,会发fin。
void tcp_close(struct sock *sk, long timeout)
{
。。。
else if (data_was_unread) {
/* Unread data was tossed, zap the connection. */
NET_INC_STATS_USER(sock_net(sk), LINUX_MIB_TCPABORTONCLOSE);
tcp_set_state(sk, TCP_CLOSE);
tcp_send_active_reset(sk, sk->sk_allocation);
}
。。。。
else if (tcp_close_state(sk)) {
tcp_send_fin(sk);
}
}
Q:开启了tw_recycle,也就是快速回收,那么回收的速度是多快呢?
void tcp_time_wait(struct sock *sk, int state, int timeo)
{
。。。
if (tcp_death_row.sysctl_tw_recycle && tp->rx_opt.ts_recent_stamp)
recycle_ok = tcp_remember_stamp(sk); if (tcp_death_row.tw_count < tcp_death_row.sysctl_max_tw_buckets)
tw = inet_twsk_alloc(sk, state); if (tw != NULL) {
struct tcp_timewait_sock *tcptw = tcp_twsk((struct sock *)tw);
const int rto = (icsk->icsk_rto << 2) - (icsk->icsk_rto >> 1);//3.5*rto
。。。
if (recycle_ok) {//开启tw 快速回收,则超时时间很短
tw->tw_timeout = rto;//这个rto其实是3.5倍的rtt
} else {
tw->tw_timeout = TCP_TIMEWAIT_LEN;
if (state == TCP_TIME_WAIT)
timeo = TCP_TIMEWAIT_LEN;
}
。。。
}
也就是说,开启recycle,则回收tw的socket时间为3.5倍的rto。
Q.timewait定时器到期后,怎么释放这些tw的资源
inet_twdr_hangman 函数负责干这事。具体可以在设置timer的时候看到,tcp_death_row 是处理所有tw状态的一个结构,包括设置定时器,锁,清理tw等。它分为快慢的两种timer,一种是正常处理2MSL的timer,一种是快速回收的tw的timer。具体可以查看 inet_twsk_schedule ,两种timer分别调用inet_twdr_hangman,inet_twdr_twcal_tick最终调用的都是 __inet_twsk_kill来回收资源。
linux tcp 在timewait 状态下的报文处理的更多相关文章
- TCP/IP协议栈(三)——linux 向下的报文处理
应用程序连接服务器时,目的地套接字地址(端口号和IP地址)以参数形式传递给系统调用connect(tcp_v4_connect()).下面逐步介绍初始化该连接 检查内核路由表,查找给定目的地IP地址路 ...
- Linux:TCP状态/半关闭/2MSL/端口复用
TCP状态 CLOSED:表示初始状态. LISTEN:该状态表示服务器端的某个SOCKET处于监听状态,可以接受连接. SYN_SENT:这个状态与SYN_RCVD遥相呼应,当客户端SOCKET执行 ...
- TCP三次握手和Time-Wait状态
第一次握手:建立连接时.client发送syn包和一个随机序列号seq=x到server,并进入SYN_SEND状态,等待server进行确认. (syn,同 步序列编号). 第二次握手,server ...
- TCP协议端口状态说明:CLOSE-WAIT、TIME-WAIT 、LISTENING、SYN_SENT、ESTABLISHED、LAST-ACK ...
了解TCP协议端口的连接状态,对排除和定位网络或系统故障会有很大帮助,因此了解一下是有必要的: 一.LISTENING 提供某种服务,侦听远方TCP端口的连接请求,当提供的服务没有被连接时,处于LI ...
- Linux TCP不同状态的连接数统计
方法一:利用netstat命令 统计 TIME_WAIT/CLOSE_WAIT/ESTABLISHED/LISTEN 等TCP状态的连接数 netstat -tan |grep ^tcp |awk ' ...
- linux 免交互状态下修改用户密码
当利用某些工具对linux用户进行远程密码更改时,输入[ passwd 用户名 ] 后需要输入两次密码, 但是如果你利用的某些工具无法与linux进行交互的情况下,就没办法变更用户密码了,这个时候可以 ...
- Linux TCP/IP调优-Linux内核参数注释
固定文件的内核参数 下列文件所在目录: /proc/sys/net/ipv4/ 名称 默认值 建议值 描述 tcpsyn_retries 5 1 对于一个新建连接,内核要发送多少个SYN连接请求才决定 ...
- tcp连接的状态变迁以及如何调整tcp连接中处于time_wait的时间
一.状态变迁图 二.time_wait状态 针对time_wait和close_wait有个简单的描述帮助理解: Due to the way TCP/IP works, connections ca ...
- 转载:TCP连接的状态详解以及故障排查
FROM:http://blog.csdn.net/hguisu/article/details/38700899 该博文的条理清晰,步骤明确,故复制到这个博文中收藏,若文章作者看到且觉得不能装载,麻 ...
随机推荐
- 软件构造Lab1实验要点总结
本实验通过解决三个问题,训练了基本的Java编程技能,并给出了Eclipse+Jdk+Junit的配置方案,以及对使用git进行项目管理的方式. 1.因此,本实验的第一个要点是配置环境.具体配置环境过 ...
- ForEach遍历集合、 集合容器
ForEach遍历集合 foreach循环是一种更加简洁的for循环,也称增强for循环,能用于遍历数组或集合中的元素. 格式: for(容器元素类型 临时变量:容器变量){ 执行语句} 从上面格式可 ...
- Win 系统下使用gnvm操作node版本
下载 gnvm官方网址 有好几种安装方式,我这里使用的是百度网盘下载. 安装 下载完成将gnvm.exe文件放到node的安装根目录下,如果你不知道安装目录在哪?可以使用命令: where node ...
- windows server2012R2 上 .net core IIS 部署--应用程序池 自动停止
在windows server2016安装部署.NET CORE时,只需要将.net core应用程序池设置无托管,然后对应你项目的版本安装一个dotnet-hosting-2.2.6-win.exe ...
- 记一次 .NET 差旅管理后台 CPU 爆高分析
一:背景 1. 讲故事 前段时间有位朋友在微信上找到我,说他的 web 系统 cpu 运行一段时候后就爆高了,让我帮忙看一下是怎么回事,那就看吧,声明一下,我看 dump 是免费的,主要是锤炼自己技术 ...
- 《吐血整理》保姆级系列教程-玩转Fiddler抓包教程(5)-Fiddler监控面板详解
1.简介 按照从上往下,从左往右的计划,今天就轮到介绍和分享Fiddler的监控面板了.监控面板主要是一些辅助标签工具栏.有了这些就会让你的会话请求和响应时刻处监控中毫无隐私可言.监控面板是fiddl ...
- windows配置skywalking集群
一.zookeeper 准备配置三个zookeeper,因为我是单台模拟,所以需要使用不同的端口,使用版本是apache-zookeeper-3.6.3-bin (必须是3.5+) 1.第1个zook ...
- 使用OpenCv+Arduino实现挂机自动打怪
使用OpenCv+Arduino实现挂机自动打怪 最近在玩某网游,练级十分枯燥和缓慢,就是挂机刷刷刷,所以研究一下自动化,找了个可以原地挂机刷怪的职业,然后用OpenCv检测技能冷却,冷却好了通过串口 ...
- SpringWeb 拦截器
前言 spring拦截器能帮我们实现验证是否登陆.验签校验请求是否合法.预先设置数据等功能,那么该如何设置拦截器以及它的原理如何呢,下面将进行简单的介绍 1.设置 HandlerInterceptor ...
- Java开发学习(十九)----AOP环绕通知案例之密码数据兼容处理
一.需求分析 需求: 对百度网盘分享链接输入密码时尾部多输入的空格做兼容处理. 问题描述: 点击链接,会提示,请输入提取码,如下图所示 当我们从别人发给我们的内容中复制提取码的时候,有时候会多复制到一 ...