主动关闭 tcp_timewait_state_process 处理
正常情况下主动关闭连接的一端在连接正常终止后,会进入TIME_WAIT状态,存在这个状态有以下两个原因(参考《Unix网络编程》):
1、保证TCP连接关闭的可靠性。如果最终发送的ACK丢失,被动关闭的一端会重传最终的FIN包,如果执行主动关闭的一端没有维护这个连接的状态信息,会发送RST包响应,导致连接不正常关闭。
2、允许老的重复分组在网络中消逝。假设在一个连接关闭后,发起建立连接的一端(客户端)立即重用原来的端口、IP地址和服务端建立新的连接。老的连接上的分组可能在新的连接建立后到达服务端,TCP必须防止来自某个连接的老的重复分组在连接终止后再现,从而被误解为同一个连接的化身。要实现这种功能,TCP不能给处于TIME_WAIT状态的连接启动新的连接。TIME_WAIT的持续时间是2MSL,保证在建立新的连接之前老的重复分组在网络中消逝。这个规则有一个例外:如果到达的SYN的序列号大于前一个连接的结束序列号,源自Berkeley的实现将给当前处于TIME_WAIT状态的连接启动新的化身。
服务器段进入TIME_WAIT后内核的处理,即服务器主动关闭连接。TCP层的接收函数是tcp_v4_rcv(),和TIME_WAIT状态相关的主要代码,
tcp_timewait_state_process这个函数具体作用,它就是分为两部分,一部分处理tw_substate== TCP_FIN_WAIT2的情况,
一部分是正常情况。在前一种情况,我们对于syn的相应是直接rst的。而后一种我们需要判断是否新建连接。
而对于fin的处理他们也是不一样的,wait2的话,它会将当前的tw重新加入到定时器列表
(inet_twsk_schedule).而后一种则只是重新发送ack。
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) || tcp_checksum_complete(skb)) { TCP_INC_STATS_BH(net, TCP_MIB_INERRS); inet_twsk_put(inet_twsk(sk)); goto discard_it; } switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) { case TCP_TW_SYN: { //取得一个sk。 struct sock *sk2 = inet_lookup_listener(dev_net(skb->dev), &tcp_hashinfo, iph->daddr, th->dest, inet_iif(skb)); if (sk2) { //从tw中删除,然后继续执行(也就是开始三次握手)。 inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row); inet_twsk_put(inet_twsk(sk)); sk = sk2; goto process; } /* Fall through to ACK */ } case TCP_TW_ACK://发送ack tcp_v4_timewait_ack(sk, skb); break; case TCP_TW_RST://发送给对端rst。 goto no_tcp_socket; case TCP_TW_SUCCESS:;//处理成功 } goto discard_it;
接收到SKb包后,会调用__inet_lookup_skb()查找对应的sock结构。如果套接字状态是TIME_WAIT状态,会跳转到do_time_wait标签处处理。从代码中可以看到,主要由tcp_timewait_state_process()函数来处理SKB包,处理后根据返回值来做相应的处理。
在看tcp_timewait_state_process()函数中的处理之前,需要先看一看不同的返回值会对应什么样的处理。
如果返回值是TCP_TW_SYN,则说明接收到的是一个“合法”的SYN包(也就是说这个SYN包可以接受),这时会首先查找内核中是否有对应的监听套接字,如果存在相应的监听套接字,则会释放TIME_WAIT状态的传输控制结构,跳转到process处开始处理,开始建立一个新的连接。如果没有找到监听套接字会执行到TCP_TW_ACK分支。
如果返回值是TCP_TW_ACK,则会调用tcp_v4_timewait_ack()发送ACK,然后跳转到discard_it标签处,丢掉数据包。
如果返回值是TCP_TW_RST,则会调用tcp_v4_send_reset()给对端发送RST包,然后丢掉数据包。
如果返回值是TCP_TW_SUCCESS,则会直接丢掉数据包。
接下来我们通过tcp_timewait_state_process()函数来看TIME_WAIT状态下的数据包处理。
为了方便讨论,假设数据包中没有时间戳选项,在这个前提下,tcp_timewait_state_process()中的局部变量paws_reject的值为0。
如果需要保持在FIN_WAIT_2状态的时间小于等于TCP_TIMEWAIT_LEN,则会从FIN_WAIT_2状态直接迁移到TIME_WAIT状态,也就是使用描述TIME_WAIT状态的sock结构代替当前的传输控制块。虽然这时的sock结构处于TIME_WAIT结构,但是还要区分内部状态,这个内部状态存储在inet_timewait_sock结构的tw_substate成员中。
/* * * Main purpose of TIME-WAIT state is to close connection gracefully, * when one of ends sits in LAST-ACK or CLOSING retransmitting FIN * (and, probably, tail of data) and one or more our ACKs are lost. * * What is TIME-WAIT timeout? It is associated with maximal packet * lifetime in the internet, which results in wrong conclusion, that * it is set to catch "old duplicate segments" wandering out of their path. * It is not quite correct. This timeout is calculated so that it exceeds * maximal retransmission timeout enough to allow to lose one (or more) * segments sent by peer and our ACKs. This time may be calculated from RTO. * * When TIME-WAIT socket receives RST, it means that another end * finally closed and we are allowed to kill TIME-WAIT too. * * Second purpose of TIME-WAIT is catching old duplicate segments. * Well, certainly it is pure paranoia, but if we load TIME-WAIT * with this semantics, we MUST NOT kill TIME-WAIT state with RSTs. * * If we invented some more clever way to catch duplicates * (f.e. based on PAWS), we could truncate TIME-WAIT to several RTOs. * * The algorithm below is based on FORMAL INTERPRETATION of RFCs. * When you compare it to RFCs, please, read section SEGMENT ARRIVES * from the very beginning. * * NOTE. With recycling (and later with fin-wait-2) TW bucket * is _not_ stateless. It means, that strictly speaking we must * spinlock it. I do not want! Well, probability of misbehaviour * is ridiculously low and, seems, we could use some mb() tricks * to avoid misread sequence numbers, states etc. --ANK */ 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; const u8 *hash_location; 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, &hash_location, 0, NULL); if (tmp_opt.saw_tstamp) { 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); } } // 如果内部状态为FIN_WAIT_2,tcp_timewait_state_process()中处理的关键代码片段如下所示:/*
tcp_timewait_state_process函数处理流程中,如果TIME_WAIT的子状态为FIN_WAIT_2,
并且收到了合法的FIN之后,会进入真正的TIME_WAIT状态,即子状态也为TIME_WAIT,
并且设置TIME_WAIT定时器;
*/ if (tw->tw_substate == TCP_FIN_WAIT2) { /* Just repeat all the checks of tcp_rcv_state_process() */ /* Out of window, send ACK 则返回TCP_TW_ACK,表示需要给对端发送ACK。序号不在窗口内或序号无效,发送 ACK */ if (paws_reject || !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_TW_ACK; if (th->rst)//收到的是RST包,则跳转到kill标签处处理,立即释放timewait控制块,并返回TCP_TW_SUCCESS。 goto kill; /* 如果是SYN包,但是SYN包的序列号在要接收的序列号之前,syn && 序号>= 期望接收序号? 则表示这是一个过期的SYN包,则跳转到kill_with_rst标签处处理, 此时不仅会释放TIME_WAIT传输控制块,还会返回TCP_TW_RST,要给对端发送RST包。 */ if (th->syn && !before(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt)) goto kill_with_rst; /* Dup ACK?如果接收到DACK,则释放timewait控制块,并返回TCP_TW_SUCCESS */ if (!th->ack || !after(TCP_SKB_CB(skb)->end_seq, tcptw->tw_rcv_nxt) || TCP_SKB_CB(skb)->end_seq == TCP_SKB_CB(skb)->seq) { inet_twsk_put(tw); return TCP_TW_SUCCESS; } /* 所以之后的处理是在数据包中的数据不为空的情况下处理。 前面的处理中已经处理了SYN包、RST包的情况,接下来就剩以下三种情况:
1、不带FIN标志的数据包 2、带FIN标志,但是还包含数据 3、FIN包,不包含数据 */ /* New data or FIN. If new data arrive after half-duplex close, * reset. 在finwait2 状态下接收到非 fin段, 或者接收到的序列号和 预期的不符合 则释放timewait 并返回 rst */ if (!th->fin || TCP_SKB_CB(skb)->end_seq != tcptw->tw_rcv_nxt + 1) { kill_with_rst: inet_twsk_deschedule(tw, &tcp_death_row); inet_twsk_put(tw); return TCP_TW_RST; } /* FIN arrived, enter true time-wait state. 如果接收的是对端的FIN包,即第3种情况,则将time_wait控制块的子状态设置为TCP_TIME_WAIT, 此时才是进入真正的TIME_WAIT状态。然后根据TIME_WAIT的持续时间的长短来确定是加入到 twcal_row队列还是启动一个定时器,最后会返回TCP_TW_ACK,给对端发送TCP连接关闭时最后 的ACK包。? 到这里,我们看到了对FIN_WAIT_2状态(传输控制块状态为TIME_WAIT状态下, 但是子状态为FIN_WAIT_2)的完整处 *//* 收到了 FIN 包,进入 TIME_WAIT 状态 */ tw->tw_substate = TCP_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; } /* 启动 TIME_WAIT 定时器 */ if (tcp_death_row.sysctl_tw_recycle && tcptw->tw_ts_recent_stamp && tcp_tw_remember_stamp(tw)) inet_twsk_schedule(tw, &tcp_death_row, tw->tw_timeout, TCP_TIMEWAIT_LEN); else inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN, TCP_TIMEWAIT_LEN); return TCP_TW_ACK; } /* * Now real TIME-WAIT state. * * 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: * * (1) assigns its initial sequence number for the new * 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". */ /* 在TIME_WAIT状态下,接收到ACK包(不带数据)或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))) { /* In window segment, it may be only reset or bare ack. */ if (th->rst) { /* This is TIME_WAIT assassination, in two flavors. * Oh well... nobody has a sufficient solution to this * protocol bug yet.如果处于TIME_WAIT状态时,受到了 Reset 包,那么,按照 TCP 协议的要求,应当重置
连接。但这里就产生了一个问题。本来TIME_WAIT之所以要等待 2MSL 的时间,就是为了
避免在网络上滞留的包对新的连接造成影响。但是,此处却可以通过发送 rst 报文强行
重置连接。重置意味着该连接会被强行关闭,跳过了 2MSL 阶段。这样就和设立 2MSL
的初衷不符了。具体的讨论见1.3.3。如果启用了 RFC1337,那么就会忽略掉这个 RST
报文。 如果是RST包的话,并且系统配置sysctl_tcp_rfc1337
(默认情况下为0,参见/proc/sys/net/ipv4/tcp_rfc1337) 的值为0,这时会立即释放time_wait传输控制块,丢掉接收的RST包。 */ if (sysctl_tcp_rfc1337 == 0) { kill: inet_twsk_deschedule(tw, &tcp_death_row); inet_twsk_put(tw); return TCP_TW_SUCCESS; } }//如果是ACK包,则会启动TIME_WAIT定时器后丢掉接收到的ACK包。 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; } /* 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. 之后是超出窗口范围的情况。 接下来是对SYN包的处理。前面提到了,如果在TIME_WAIT状态下接收到序列号比 上一个连接的结束序列号大的SYN包,可以接受,并建立新的连接, */ 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)->when = isn; return TCP_TW_SYN; } if (paws_reject) NET_INC_STATS_BH(twsk_net(tw), LINUX_MIB_PAWSESTABREJECTED); /* 下面这段代码就是来处理这样的情况: ? 当返回TCP_TW_SYN时,在tcp_v4_rcv()中会立即释放time_wait控制块, 并且开始进行正常的连接建立过程。? 如果数据包不是上述几种类型的包, 可能的情况有:? 1、不是有效的SYN包。不考虑时间戳的话, 就是序列号在上一次连接的结束序列号之前? 2、ACK包,起始序列号不是下 一个要接收的序列号? 3、RST包,起始序列号不是下一个要接收的序列号? 4、带数据的SKB包 */ if (!th->rst) { /* 如果带有ACK标志的话,则会启动TIME_WAIT定时器,然后给对端发送ACK。我们知道SYN包正常情况下不会设置ACK标志, 所以如果是SYN包不会启动TIME_WAIT定时器,只会给对端发送ACK,告诉对端已经收到SYN包,避免重传,但连接应该不会继续建立。 ? 还有一个细节需要提醒下,就是我们看到在返回TCP_TW_ACK时,没有调用inet_twsk_put()释放对time_wait控制块的引用。 这时因为在tcp_v4_rcv()中调用tcp_v4_timewait_ack()发送ACK时会用到time_wait控制块,所以需要保持对time_wait控制块的引用。 在tcp_v4_timewait_ack()中发送完ACK后,会调用inet_twsk_put()释放对time_wait控制块的引用。 */ /* 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.
收到了序号绕回的包,那么就重置TIME_WAIT定时器,并返回 TCP_TW_ACK */ if (paws_reject || th->ack) inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN, TCP_TIMEWAIT_LEN); /* Send ACK. Note, we do not put the bucket, * it will be released by caller. */ return TCP_TW_ACK; } // 如果是RST包,即第3种情况,则直接返回TCP_TW_SUCCESS,丢掉RST包。 inet_twsk_put(tw); return TCP_TW_SUCCESS; }
这只是 站在服务端的角度看问题 !!!
那么站在客户端呢??????????
又是怎样
总结一下:
在 TCP 正常挥手过程中,处于 TIME_WAIT 状态的连接,收到相同四元组的 SYN OR RST后会发生什么?
如果双方开启了时间戳机制:
- 如果客户端的 SYN 的「序列号」比服务端「期望下一个收到的序列号」要大,并且SYN 的「时间戳」比服务端「最后收到的报文的时间戳」要大。那么就会重用该四元组连接,跳过 2MSL 而转变为 SYN_RECV 状态,接着就能进行建立连接过程。
- 如果客户端的 SYN 的「序列号」比服务端「期望下一个收到的序列号」要小,或者SYN 的「时间戳」比服务端「最后收到的报文的时间戳」要小。那么就会再回复一个第四次挥手的 ACK 报文,客户端收到后,发现并不是自己期望收到确认号,就回 RST 报文给服务端。
- 如果双方都没有启用 TCP 时间戳机制,就只需要判断 SYN 包的序列号有没有发生回绕,如果 SYN 的序列号大于下一次期望收到的序列号,就可以跳过 2MSL,重用该连接。
- SYN 包是非法的,就会返回 TCP_TW_ACK,接着就会发送与上一次一样的 ACK 给对方
- 收到了序号绕回的包,那么就重置TIME_WAIT定时器,并返回 TCP_TW_ACK
-
处于 TIME_WAIT 状态的连接,收到 RST 会不会断开,关键看
net.ipv4.tcp_rfc1337
这个内核参数(默认情况是为 0):- 如果这个参数设置为 0, 收到 RST 报文会提前结束 TIME_WAIT 状态,释放连接。
- 如果这个参数设置为 1, 就会丢掉 RST 报文。
在 TCP 正常挥手过程中,处于 FIN_WAIT2状态的连接,收到相同四元组的 SYN OR RST后会发生什么?
- 如果收到 RST 包,则销毁 timewait 控制块并返回 TCP_TW_SUCCESS
- 如果收到过期的 SYN 包,则销毁并发送 RST
- 如果收到 DACK,则释放该控制块,返回 TCP_TW_SUCCESS
- 收到了新的数据或者序号有问题,则销毁控制块并返回 TCP_TW_RST
- 收到了 FIN 包,进入 TIME_WAIT 状态