问题背景
再次回想起这个问题,发现都是两年前的事情了,应该是刚开始研究 packetdrill 时偶尔所发现的现象,也是琢磨了很久,关于服务器在收到 SYN 并且响应 SYN/ACK 后,又再次收到 SYN ,是如何一个处理过程。
当时是和科来的熊猫君老师一起探讨,通过不断的实验测试发现了数据包的一些规律,最后定位到了 500ms 这个特殊值,但当时对内核协议栈一窍不通,所以并不是很清楚具体原理。之后也是机缘巧合下,在知乎上认识的一位老师(评谈网络技术)帮忙解答了代码层面的实现,他也专门写了一篇文章《杂谈:TCP服务端SYN_ACK发出后再收到客户端的SYN会怎么样?》进行了相关说明,到那时问题才算最终解决,再次感谢两位老师!
一直没有总结说明这个问题,此次结合之前关于《TCP 三次握手之 SYN/ACK 超时重传》中的研究结论,再把这个问题回顾一下。
基础脚本
# cat tcp_3hs_000.pkt
// TCP 基础之三次握手
0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 bind(3, ..., ...) = 0
+0 listen(3, 1) = 0
+0 < S 0:0(0) win 10000 <mss 1460>
+0 > S. 0:0(0) ack 1 <...>
+0.01 < . 1:1(0) ack 1 win 10000
+0 accept(3, ..., ...) = 4
同时通过 sysctl 修改 SYN/ACK 的重传次数,减少等待。
# sysctl -q net.ipv4.tcp_synack_retries=3
# sysctl -a | grep synack
net.ipv4.tcp_synack_retries = 3
实验测试一
首先在 TCP 三次握手基础脚本之上进行一定修改,间隔 10ms 注入 SYN,并且不判断 SYN/ACK 的响应以及不注入第三次握手 ACK。
# cat tcp_3hs_oow_001.pkt
0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 bind(3, ..., ...) = 0
+0 listen(3, 1) = 0
+0 < S 0:0(0) win 16000 <mss 1460>
+0.01 < S 0:0(0) win 16000 <mss 1460>
+0.01 < S 0:0(0) win 16000 <mss 1460>
+0.01 < S 0:0(0) win 16000 <mss 1460>
+0 `sleep 100`
通过 tcpdump 捕获数据包后,经 Wireshark 显示如下:
- No.1 客户端 SYN 和 No.2 服务器 SYN/ACK;
- No.3 客户端间隔 10ms 后,再次发出 SYN,此时服务器立马响应 No.4 SYN/ACK;
- No.5 客户端间隔 10ms 后,再次发出 SYN,此时服务器并不响应;
- No.6 客户端间隔 10ms 后,再次发出 SYN,此时服务器并不响应;
- No.7 服务器 SYN/ACK 实际为 No.4 SYN/ACK 的超时重传,间隔 1s;
- No.8 服务器 SYN/ACK 实际为 No.7 SYN/ACK 的超时重传,间隔 2s;
- No.9 服务器 SYN/ACK 实际为 No.8 SYN/ACK 的超时重传,间隔 4s,至此结束。
继续修改 SYN 间隔时间,间隔 100ms 注入 SYN,同样不判断 SYN/ACK 的响应以及不注入第三次握手 ACK,实验结果并无不同。
# cat tcp_3hs_oow_002.pkt
0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 bind(3, ..., ...) = 0
+0 listen(3, 1) = 0
+0 < S 0:0(0) win 16000 <mss 1460>
+0.1 < S 0:0(0) win 16000 <mss 1460>
+0.1 < S 0:0(0) win 16000 <mss 1460>
+0.1 < S 0:0(0) win 16000 <mss 1460>
+0 `sleep 100`
实验测试二
仍以间隔 100ms 注入 SYN,但次数增加到 7 次。
# cat tcp_3hs_oow_003.pkt
0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 bind(3, ..., ...) = 0
+0 listen(3, 1) = 0
+0 < S 0:0(0) win 16000 <mss 1460>
+0.1 < S 0:0(0) win 16000 <mss 1460>
+0.1 < S 0:0(0) win 16000 <mss 1460>
+0.1 < S 0:0(0) win 16000 <mss 1460>
+0.1 < S 0:0(0) win 16000 <mss 1460>
+0.1 < S 0:0(0) win 16000 <mss 1460>
+0.1 < S 0:0(0) win 16000 <mss 1460>
+0 `sleep 100`
通过 tcpdump 捕获数据包后,经 Wireshark 显示如下:
- No.1 客户端 SYN 和 No.2 服务器 SYN/ACK;
- No.3 客户端间隔 100ms 后,再次发出 SYN,此时服务器立马响应 No.4 SYN/ACK;
- No.5 客户端间隔 100ms 后,再次发出 SYN,此时服务器并不响应;
- No.6 客户端间隔 100ms 后,再次发出 SYN,此时服务器并不响应;
- No.7 客户端间隔 100ms 后,再次发出 SYN,此时服务器并不响应;
- No.8 客户端间隔 100ms 后,再次发出 SYN,此时服务器并不响应;
- No.9 客户端间隔 100ms 后,再次发出 SYN,此时服务器立马响应 No.10 SYN/ACK;
- No.11 服务器 SYN/ACK 实际为 No.10 SYN/ACK 的超时重传,间隔 1s;
- No.12 服务器 SYN/ACK 实际为 No.11 SYN/ACK 的超时重传,间隔 2s;
- No.13 服务器 SYN/ACK 实际为 No.12 SYN/ACK 的超时重传,间隔 4s,至此结束。
上述实验二反复测试了几次后,隐约发现了一个时间间隔规律,服务器能再次响应 SYN/ACK 的时候,也就是 No.10,离上次响应SYN/ACK No.4 的间隔时间为 500ms 。
实验测试三
在发现了 500ms 这个时间间隔后,继续以两个实验做了相关验证,第一次 SYN 间隔时间如下:
# cat tcp_3hs_oow_004.pkt
0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 bind(3, ..., ...) = 0
+0 listen(3, 1) = 0
+0 < S 0:0(0) win 16000 <mss 1460>
+0.1 < S 0:0(0) win 16000 <mss 1460>
+0.5 < S 0:0(0) win 16000 <mss 1460>
+0.5 < S 0:0(0) win 16000 <mss 1460>
+0 `sleep 100`
通过 tcpdump 捕获数据包后,经 Wireshark 显示如下:
- No.1 客户端 SYN 和 No.2 服务器 SYN/ACK;
- No.3 客户端间隔 100ms 后,再次发出 SYN,此时服务器立马响应 No.4 SYN/ACK;
- No.5 客户端间隔 500ms 后,再次发出 SYN,此时服务器立马响应 No.6 SYN/ACK;
- No.7 客户端间隔 500ms 后,再次发出 SYN,此时服务器立马响应 No.8 SYN/ACK;
- No.9 服务器 SYN/ACK 实际为 No.8 SYN/ACK 的超时重传,间隔 1s;
- No.10 服务器 SYN/ACK 实际为 No.9 SYN/ACK 的超时重传,间隔 2s;
- No.11 服务器 SYN/ACK 实际为 No.10 SYN/ACK 的超时重传,间隔 4s,至此结束。
# cat tcp_3hs_oow_005.pkt
0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 bind(3, ..., ...) = 0
+0 listen(3, 1) = 0
+0 < S 0:0(0) win 16000 <mss 1460>
+0.1 < S 0:0(0) win 16000 <mss 1460>
+0.49 < S 0:0(0) win 16000 <mss 1460>
+0.02 < S 0:0(0) win 16000 <mss 1460>
+0 `sleep 100`
通过 tcpdump 捕获数据包后,经 Wireshark 显示如下:
- No.1 客户端 SYN 和 No.2 服务器 SYN/ACK;
- No.3 客户端间隔 100ms 后,再次发出 SYN,此时服务器立马响应 No.4 SYN/ACK;
- No.5 客户端间隔 490ms 后,再次发出 SYN,此时服务器并不响应;
- No.6 客户端间隔 20ms 后,再次发出 SYN,此时服务器立马响应 No.7 SYN/ACK;
- No.8 服务器 SYN/ACK 实际为 No.7 SYN/ACK 的超时重传,间隔 1s;
- No.9 服务器 SYN/ACK 实际为 No.8 SYN/ACK 的超时重传,间隔 2s;
- No.10 服务器 SYN/ACK 实际为 No.9 SYN/ACK 的超时重传,间隔 4s,至此结束。
综合以上实验,可知服务器在间隔上次响应 SYN/ACK 500ms 后,才会对重传的 SYN 再次响应发出 SYN/ACK,而如果在 500ms 时间间隔之内,再次收到 SYN 的情况下,则不会响应 SYN/ACK。
注:安全方向的同学可能对此会更加熟悉,场景就像是 SYN Flood 攻击下,服务器由于安全策略控制了响应 SYN/ACK 的速率。
代码实现
服务器端在第一次收到 SYN 并发送 SYN/ACK 后,进入了 TCP_NEW_SYN_RECV 状态,再次收到 SYN 后,通过 tcp_check_req() 进行检查。
int tcp_v4_rcv(struct sk_buff *skb)
{
struct net *net = dev_net(skb->dev);
int sdif = inet_sdif(skb);
const struct iphdr *iph;
const struct tcphdr *th;
bool refcounted;
struct sock *sk;
int ret;
...
process:
if (sk->sk_state == TCP_TIME_WAIT)
goto do_time_wait;
if (sk->sk_state == TCP_NEW_SYN_RECV) {
struct request_sock *req = inet_reqsk(sk);
bool req_stolen = false;
struct sock *nsk;
sk = req->rsk_listener;
if (unlikely(tcp_v4_inbound_md5_hash(sk, skb))) {
sk_drops_add(sk, skb);
reqsk_put(req);
goto discard_it;
}
if (tcp_checksum_complete(skb)) {
reqsk_put(req);
goto csum_error;
}
if (unlikely(sk->sk_state != TCP_LISTEN)) {
inet_csk_reqsk_queue_drop_and_put(sk, req);
goto lookup;
}
/* We own a reference on the listener, increase it again
* as we might lose it too soon.
*/
sock_hold(sk);
refcounted = true;
nsk = NULL;
if (!tcp_filter(sk, skb)) {
th = (const struct tcphdr *)skb->data;
iph = ip_hdr(skb);
tcp_v4_fill_cb(skb, iph, th);
nsk = tcp_check_req(sk, skb, req, false, &req_stolen);
}
...
}
}
在 tcp_check_req 中对于纯重传 SYN(pure retransmitted SYN)的处理如下,首先判断重传 SYN,需满足以下条件:
- 检查收到的包的序列号是否等于初始接收序列号(ISN);
- 确认数据包只设置了SYN标志;
- 确保没有被 PAWS(Protection Against Wrapped Sequence numbers)机制拒绝。
之后会继续检查,也是本篇文章的重点,判断是否超过了发送响应的速率限制,这是一种防御机制,防止过多的响应可能导致的资源耗尽。如果没有超过速率限制,尝试重新发送 SYN/ACK。
/*
* Process an incoming packet for SYN_RECV sockets represented as a
* request_sock. Normally sk is the listener socket but for TFO it
* points to the child socket.
*
* XXX (TFO) - The current impl contains a special check for ack
* validation and inside tcp_v4_reqsk_send_ack(). Can we do better?
*
* We don't need to initialize tmp_opt.sack_ok as we don't use the results
*/
struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
bool fastopen, bool *req_stolen)
{
struct tcp_options_received tmp_opt;
struct sock *child;
const struct tcphdr *th = tcp_hdr(skb);
__be32 flg = tcp_flag_word(th) & (TCP_FLAG_RST|TCP_FLAG_SYN|TCP_FLAG_ACK);
bool paws_reject = false;
bool own_req;
tmp_opt.saw_tstamp = 0;
if (th->doff > (sizeof(struct tcphdr)>>2)) {
tcp_parse_options(sock_net(sk), skb, &tmp_opt, 0, NULL);
if (tmp_opt.saw_tstamp) {
tmp_opt.ts_recent = req->ts_recent;
if (tmp_opt.rcv_tsecr)
tmp_opt.rcv_tsecr -= tcp_rsk(req)->ts_off;
/* We do not store true stamp, but it is not required,
* it can be estimated (approximately)
* from another data.
*/
tmp_opt.ts_recent_stamp = ktime_get_seconds() - ((TCP_TIMEOUT_INIT/HZ)<<req->num_timeout);
paws_reject = tcp_paws_reject(&tmp_opt, th->rst);
}
}
/* Check for pure retransmitted SYN. */
if (TCP_SKB_CB(skb)->seq == tcp_rsk(req)->rcv_isn &&
flg == TCP_FLAG_SYN &&
!paws_reject) {
/*
* RFC793 draws (Incorrectly! It was fixed in RFC1122)
* this case on figure 6 and figure 8, but formal
* protocol description says NOTHING.
* To be more exact, it says that we should send ACK,
* because this segment (at least, if it has no data)
* is out of window.
*
* CONCLUSION: RFC793 (even with RFC1122) DOES NOT
* describe SYN-RECV state. All the description
* is wrong, we cannot believe to it and should
* rely only on common sense and implementation
* experience.
*
* Enforce "SYN-ACK" according to figure 8, figure 6
* of RFC793, fixed by RFC1122.
*
* Note that even if there is new data in the SYN packet
* they will be thrown away too.
*
* Reset timer after retransmitting SYNACK, similar to
* the idea of fast retransmit in recovery.
*/
if (!tcp_oow_rate_limited(sock_net(sk), skb,
LINUX_MIB_TCPACKSKIPPEDSYNRECV,
&tcp_rsk(req)->last_oow_ack_time) &&
!inet_rtx_syn_ack(sk, req)) {
unsigned long expires = jiffies;
expires += min(TCP_TIMEOUT_INIT << req->num_timeout,
TCP_RTO_MAX);
if (!fastopen)
mod_timer_pending(&req->rsk_timer, expires);
else
req->rsk_timer.expires = expires;
}
return NULL;
}
...
}
EXPORT_SYMBOL(tcp_check_req);
在 tcp_oow_rate_limited 中,第一次收到重传的 SYN 时,*last_oow_ack_time 为 0,if 条件不满足,更新 last_oow_ack_time 为当前时间(tcp_jiffies32),并返回 false,返回 false 意味着"不限制速率",并尝试发送 SYN/ACK,成功发送则返回 0,取反后条件为 true。这也是上述实验中,不管间隔多长时间,在第一次收到重传 SYN 时,服务器都会立马响应发送 SYN/ACK。
/* Return true if we're currently rate-limiting out-of-window ACKs and
* thus shouldn't send a dupack right now. We rate-limit dupacks in
* response to out-of-window SYNs or ACKs to mitigate ACK loops or DoS
* attacks that send repeated SYNs or ACKs for the same connection. To
* do this, we do not send a duplicate SYNACK or ACK if the remote
* endpoint is sending out-of-window SYNs or pure ACKs at a high rate.
*/
bool tcp_oow_rate_limited(struct net *net, const struct sk_buff *skb,
int mib_idx, u32 *last_oow_ack_time)
{
/* Data packets without SYNs are not likely part of an ACK loop. */
if ((TCP_SKB_CB(skb)->seq != TCP_SKB_CB(skb)->end_seq) &&
!tcp_hdr(skb)->syn)
return false;
return __tcp_oow_rate_limited(net, mib_idx, last_oow_ack_time);
}
static bool __tcp_oow_rate_limited(struct net *net, int mib_idx,
u32 *last_oow_ack_time)
{
if (*last_oow_ack_time) {
s32 elapsed = (s32)(tcp_jiffies32 - *last_oow_ack_time);
if (0 <= elapsed && elapsed < net->ipv4.sysctl_tcp_invalid_ratelimit) {
NET_INC_STATS(net, mib_idx);
return true; /* rate-limited: don't send yet! */
}
}
*last_oow_ack_time = tcp_jiffies32;
return false; /* not rate-limited: go ahead, send dupack now! */
}
而在继续注入 SYN 时,服务器会与上次收到 SYN 且发送 SYN/ACK 的间隔时间进行比较,满足 elapsed < net->ipv4.sysctl_tcp_invalid_ratelimit 条件后,返回 true ,返回 true 意味着"限制速率",所以不会响应发送 SYN/ACK,而 net.ipv4.tcp_invalid_ratelimit 的默认值为 500ms,所以这证明了上述实验中的判断结果正确。
# sysctl -a | grep invalid
net.ipv4.tcp_invalid_ratelimit = 500
#
验证总结
结合 net.ipv4.tcp_invalid_ratelimit 的值,总结验证下上述结论,修改为 800ms。
# sysctl -q net.ipv4.tcp_invalid_ratelimit=800
# sysctl -a | grep invalid
net.ipv4.tcp_invalid_ratelimit = 800
#
更改测试代码如下后,收到 No.5 客户端 SYN 时,间隔 790ms < 800ms 限制速率,服务器不发送 SYN/ACK,无响应。而收到 No.6 客户端 SYN 时, 810ms > 800ms 不限制速率,服务器响应发送 SYN/ACK,即 No.7 。
# cat tcp_3hs_oow_006.pkt
0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 bind(3, ..., ...) = 0
+0 listen(3, 1) = 0
+0 < S 0:0(0) win 16000 <mss 1460>
+0.1 < S 0:0(0) win 16000 <mss 1460>
+0.79 < S 0:0(0) win 16000 <mss 1460>
+0.02 < S 0:0(0) win 16000 <mss 1460>
+0 `sleep 100`
原文始发于微信公众号(Echo Reply):Wireshark TS | 服务器发送 SYN/ACK 后又收到 SYN 如何处理
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论