SRE、运维工程师面试过程中,会问哪些问题?特别是TCP网络一直都是面试的高发题目,会被问些啥呢,这里我整理了相关的高发问题。
所有的问题会陆续更新到WX公众号:Linux运维实战派
或扫描关注,获取最新更新
一线大厂面试真题:
大厂面试必问:CPU使用率100%怎么办?运维高手这样回答!
字节面试真题–用户反馈网页访问慢,可能会有哪些原因
字节面试真题–TCP建立连接为什么要三次握手?为什么不是两次或者四次?
面试八股(五) TCP的三次握手与四次挥手过程【是详述可不是简单几句话哦】
面试八股(六) 腾讯面试真题:close_wait状态的原因是什么?该怎么办?
面试八股(七) TCP Keepalive和HTTP Keep-Alive有什么区别?
面试八股(八) Linux服务器拔网线或者断电,已建立的TCP连接会中断吗?
面试八股(九) 关于TCP TIME_WAIT状态的所有面试题都在这里了
面试八股(11) 字节真题,关于TCP半连接与全连接队列的所有面试题
面试八股(12) 大厂真题,你了解synflood攻击吗?该如何应对
问题10:服务端处于TCP TIME_WAIT状态的连接,客户端重用这个连接还能连上吗?
书接上回,在服务端处于TIME_WAIT状态时,如果客户端使用了相同的四元组来尝试建立连接(接收到SYN包),会怎么样?会被reset吗?
下边我会结合kernel 5.10的源码分析结果。
一、问题结论
服务端收到合法的SYN会被接收并正常建立连接
服务端收到非法的SYN包后,会向客户端回复ACK,因为客户端此时连接已经关闭(或SYN_SENT状态),所以这个ACK会被客户端reset
二、合法SYN包
接收到的数据包的时间戳比上一次连接中接收的数据包时间戳大,且数据包的序列号要比上一次连接中接收的数据包最后的序列号要大
三、非法SYN包
收到非法的SYN包的时候
四、服务端TIME_WAIT状态的异常场景
4.1 问题场景
在上边的场景中,就是经过了NAT,或者LVS(LVS的fnat),NAT或者LVS节点的local ip比较少,有的甚至只有一个。目标服务也只有一个实例,或者很少几个
-
第一个client请求过来的时候,经过NAT或者LVS NAT之后,使用的local ip是一个。请求到Server之后,记录下了这个四元组的序号和时间戳。
-
如果Server服务频繁的中断连接,出现大量的TIME_WAIT连接
-
在高并发的场景下,在后续的第二个Client过来请求的时候,如果出现了端口回绕(四元组源IP,目标IP,目标端口是固定的),使用相同的源端口(23456)到达Service(服务上的四元组正处于TIME_WAIT)
-
如果后续Client的时间戳比Service上记录的时间戳小,这个时候就会触发非法SYN包的逻辑,客户端会收到ACK报文。客户端将这个ACK进行RST,最终导致时钟慢的主机,会连接失败。
4.2 出现概率
-
NAT/LVS上的local ip比较少,后端服务Service节点太少的时候,造成四元组只有源端口一个可变因素
-
高并发的短连接(服务端主动关闭)造成大量的TIME_WAIT
4.3 怎么解决
-
NAT/LVS增加local ip,在之前LVS的Fullnat结构中,每个LVS实例都会配置一整个C段作为源IP
-
扩容后端Server的节点数量
-
采用长连接【很重要】
五、源码分析
基于kernel 5.10源码分析,TCP收包的入口函数tcp_v4_rcv
time_wait状态的处理流程:
tcp_v4_rcv
|- __inet_lookup_skb
|- tcp_timewait_state_process
int tcp_v4_rcv(struct sk_buff *skb)
{
....
// 查找对应的sock
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
....
// 判断sock的状态为TIME_WAIT
if (sk->sk_state == TCP_TIME_WAIT)
goto do_time_wait;
....
do_time_wait:
switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {
case TCP_TW_SYN: {
// 允许接收SYN包,重建连接,TIME_WAIT会流转到SYN_RECV
}
case TCP_TW_ACK:
// 返回上一次发送的ACK报文(收到最后一个FIN后发送的ACK报文)
tcp_v4_timewait_ack(sk, skb);
break;
case TCP_TW_RST:
// 发送RST,并销毁TIME_WAIT,直接回收TW状态,进入CLOSED状态
tcp_v4_send_reset(sk, skb);
inet_twsk_deschedule_put(inet_twsk(sk));
goto discard_it;
case TCP_TW_SUCCESS:;
// 忽略数据包,不做任何回复
}
上边代码主要的两个分支,一个是TCP_TW_SYN,这个这个状态的时候,则会针对这个SYN包回复正常的SYN+ACK,服务端会从TIME_WAIT状态直接转换到 SYN_RECV状态。
TCP_TW_ACK这个状态的时候,服务端会回复ACK数据包(就是服务端最后收到FIN包回复的那个ACK报文)给客户端
那这两个状态是怎么来的?就需要分析tcp_timewait_state_process 函数了,核心流程如下:
enum tcp_tw_statustcp_timewait_state_process(struct inet_timewait_sock *tw, struct sk_buff *skb, const struct tcphdr *th)
{
bool paws_reject = false;
tmp_opt.saw_tstamp = 0;
....
if (tmp_opt.saw_tstamp) {
....
// 接收数据包的时间戳比上一次接收的时间戳大检查通过 == false
// 或者上一次接收的数据包没有时间戳,也会检查通过 == false
// 对于reset报文,则判断上一次接收到数据包的时间戳是否已经过了TIMEWAIT_LEN的时间(60s)。如果已经过了60s,表明RST是一个旧的数据包,检查通过 == false
paws_reject = tcp_paws_reject(&tmp_opt, th->rst);
}
}
if (tw->tw_substate == TCP_FIN_WAIT2) {
// paws检查不通过==true,或者不在接收窗口中的,会回复上一次记录的ACK
// tcp_timewait_check_oow_rate_limit 函数中,会检查数据包有没有被流控,没有的话,会发送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_timewait_check_oow_rate_limit(
tw, skb, LINUX_MIB_TCPACKSKIPPEDFINWAIT2);
// FIN_WAIT2接收到rest报文,则不管是什么情况,直接回收连接
if (th->rst)
goto kill;
// FIN_WAIT2状态接收到SYN包,并且SYN包序列号比期望的下一个数据包序号要大
// 因为此时在等待对方发送数据或者FIN包关闭,SYN包是需要拒绝的
if (th->syn && !before(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt))
return TCP_TW_RST;
// 不是FIN包,或者数据序号不是自己期望的,回复RST
if (!th->fin ||TCP_SKB_CB(skb)->end_seq != tcptw->tw_rcv_nxt + 1)
return TCP_TW_RST;
// 接收FIN包,最终进入TIME_WAIT状态,并回复最后一个ACK
tw->tw_substate = TCP_TIME_WAIT;
....
inet_twsk_reschedule(tw, TCP_TIMEWAIT_LEN);
return TCP_TW_ACK;
}
// TIME_WAIT状态中,paws时间戳检查通过。
// 不带数据ack报文或者reset报文
// 对于rest报文,会根据ipv4.sysctl_tcp_rfc1337判断执行逻辑,默认值为0,会直接回收time_wait。为1的时候,会重置time_wait时间
// 其他满足条件的报文(如0大小的ack)会被忽略
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))) {
if (th->rst) {
if (twsk_net(tw)->ipv4.sysctl_tcp_rfc1337 == 0) {
kill:
// 直接回收time_wait状态
inet_twsk_deschedule_put(tw);
return TCP_TW_SUCCESS;
}
} else {
// 重置time_wait超时时间
inet_twsk_reschedule(tw, TCP_TIMEWAIT_LEN);
}
return TCP_TW_SUCCESS;
}
// 如果在TIME_WAIT时接收到了SYN包,并且时间戳paws检查通过,并且序号要比最近保存的序列号要大 // 或者时间戳存在并且时间戳比最近接收的数据包的时间戳要大
// 那这个时候会重置序号,并正常建立连接,转到SYN_RECV状态
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))) {
....
return TCP_TW_SYN;
}
// 对于时间戳paws检查不通过的请求,会进行计数
if (paws_reject)
__NET_INC_STATS(twsk_net(tw), LINUX_MIB_PAWSESTABREJECTED);
// 不满足上边条件的数据包,如非法的SYN包(旧的,或者SYN的序列号比期望的序号小)
// 进入到tcp_timewait_check_oow_rate_limit,当没有被限速的时候会返回ACK
if (!th->rst) {
// paws时间戳检查不通过,或者包含ack,则将TIME_WAIT持续时间延长
if (paws_reject || th->ack)
inet_twsk_reschedule(tw, TCP_TIMEWAIT_LEN);
return tcp_timewait_check_oow_rate_limit(
tw, skb, LINUX_MIB_TCPACKSKIPPEDTIMEWAIT);
/*
这个函数定义比较简单,就去判断有么有超过限速,超过了忽略,没有则返回上一次的ACK包
tcp_timewait_check_oow_rate_limit(struct inet_timewait_sock *tw,
const struct sk_buff *skb, int mib_idx) {
if (不超过限速) {
return TCP_TW_ACK;
}
return TCP_TW_SUCCESS;
}
*/
}
inet_twsk_put(tw);
return TCP_TW_SUCCESS;
}
- 出于FIN_WAIT2状态时PAWS时间戳校验不通过,或者不在接受窗口的数据包,会回复通过ACK报文,标明自己期望接受的数据包序号【这里其实也是TCP协议的一个安全风险】
- FIN_WAIT2接收到到正常的FIN包后,会发送ACK,并进入到TIME_WAIT,并记录下最后收到的FIN包的时间戳和序号
- 在FIN_WAIT2下接受到RST报文时,会直接进行RST
- 在TIME_WAIT状态中接收到SYN包之后,会判断时间戳是否比最后记录的FIN包的时间戳要更新(PAWS检查),并且序号比最后收到的FIN包的序号大
after(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt)
。那就会回复SYN+ACK
,正常建立连接【大部分生产环境下都是可以满足的】 - 其他不满足条件的SYN包,都会响应
ACK
报文,标明服务端期望接收的数据序号,也就是服务端接受到最后一个FIN
包回复的那个ACK报文 - TIME_WAIT状态中,如果接收到
RST
报文,具体这个RST
报文会不会被处理,取决于sysctl参数net.ipv4.tcp_rfc1337
的配置- 如果参数为
0
,RST报文会被处理,直接重置TIME_WAIT
连接,进入CLOSED
状态 - 如果参数为
1
,RST报文会被忽略
- 如果参数为