Wireshark TS | 服务器发送 SYN/ACK 后又收到 SYN 如何处理

admin 2024年7月29日15:21:02评论24 views字数 9627阅读32分5秒阅读模式
时间带来的积累是最稳固的

问题背景

再次回想起这个问题,发现都是两年前的事情了,应该是刚开始研究 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 synacknet.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 显示如下:

  1. No.1 客户端 SYN 和 No.2 服务器 SYN/ACK;
  2. No.3 客户端间隔 10ms 后,再次发出 SYN,此时服务器立马响应 No.4 SYN/ACK
  3. No.5 客户端间隔 10ms 后,再次发出 SYN,此时服务器并不响应;
  4. No.6 客户端间隔 10ms 后,再次发出 SYN,此时服务器并不响应;
  5. No.7 服务器 SYN/ACK 实际为 No.4 SYN/ACK 的超时重传,间隔 1s;
  6. No.8 服务器 SYN/ACK 实际为 No.7 SYN/ACK 的超时重传,间隔 2s;
  7. No.9 服务器 SYN/ACK 实际为 No.8 SYN/ACK 的超时重传,间隔 4s,至此结束。

Wireshark TS | 服务器发送 SYN/ACK 后又收到 SYN 如何处理

继续修改 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`

Wireshark TS | 服务器发送 SYN/ACK 后又收到 SYN 如何处理

实验测试二

仍以间隔 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 显示如下:

  1. No.1 客户端 SYN 和 No.2 服务器 SYN/ACK;
  2. No.3 客户端间隔 100ms 后,再次发出 SYN,此时服务器立马响应 No.4 SYN/ACK
  3. No.5 客户端间隔 100ms 后,再次发出 SYN,此时服务器并不响应;
  4. No.6 客户端间隔 100ms 后,再次发出 SYN,此时服务器并不响应;
  5. No.7 客户端间隔 100ms 后,再次发出 SYN,此时服务器并不响应;
  6. No.8 客户端间隔 100ms 后,再次发出 SYN,此时服务器并不响应;
  7. No.9 客户端间隔 100ms 后,再次发出 SYN,此时服务器立马响应 No.10 SYN/ACK
  8. No.11 服务器 SYN/ACK 实际为 No.10 SYN/ACK 的超时重传,间隔 1s;
  9. No.12 服务器 SYN/ACK 实际为 No.11 SYN/ACK 的超时重传,间隔 2s;
  10. No.13 服务器 SYN/ACK 实际为 No.12 SYN/ACK 的超时重传,间隔 4s,至此结束。

Wireshark TS | 服务器发送 SYN/ACK 后又收到 SYN 如何处理

上述实验二反复测试了几次后,隐约发现了一个时间间隔规律服务器能再次响应 SYN/ACK 的时候,也就是 No.10,离上次响应SYN/ACK No.4 的间隔时间为 500ms

Wireshark TS | 服务器发送 SYN/ACK 后又收到 SYN 如何处理

实验测试三

在发现了 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 显示如下:

  1. No.1 客户端 SYN 和 No.2 服务器 SYN/ACK;
  2. No.3 客户端间隔 100ms 后,再次发出 SYN,此时服务器立马响应 No.4 SYN/ACK
  3. No.5 客户端间隔 500ms 后,再次发出 SYN,此时服务器立马响应 No.6 SYN/ACK
  4. No.7 客户端间隔 500ms 后,再次发出 SYN,此时服务器立马响应 No.8 SYN/ACK
  5. No.9 服务器 SYN/ACK 实际为 No.8 SYN/ACK 的超时重传,间隔 1s;
  6. No.10 服务器 SYN/ACK 实际为 No.9 SYN/ACK 的超时重传,间隔 2s;
  7. No.11 服务器 SYN/ACK 实际为 No.10 SYN/ACK 的超时重传,间隔 4s,至此结束。

Wireshark TS | 服务器发送 SYN/ACK 后又收到 SYN 如何处理

第二次 SYN 间隔时间如下:
# 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 显示如下:

  1. No.1 客户端 SYN 和 No.2 服务器 SYN/ACK;
  2. No.3 客户端间隔 100ms 后,再次发出 SYN,此时服务器立马响应 No.4 SYN/ACK
  3. No.5 客户端间隔 490ms 后,再次发出 SYN,此时服务器并不响应
  4. No.6 客户端间隔 20ms 后,再次发出 SYN,此时服务器立马响应 No.7 SYN/ACK
  5. No.8 服务器 SYN/ACK 实际为 No.7 SYN/ACK 的超时重传,间隔 1s;
  6. No.9 服务器 SYN/ACK 实际为 No.8 SYN/ACK 的超时重传,间隔 2s;
  7. No.10 服务器 SYN/ACK 实际为 No.9 SYN/ACK 的超时重传,间隔 4s,至此结束。

Wireshark TS | 服务器发送 SYN/ACK 后又收到 SYN 如何处理

Wireshark TS | 服务器发送 SYN/ACK 后又收到 SYN 如何处理

综合以上实验,可知服务器在间隔上次响应 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 invalidnet.ipv4.tcp_invalid_ratelimit = 500# 

验证总结

结合 net.ipv4.tcp_invalid_ratelimit 的值,总结验证下上述结论,修改为 800ms。

# sysctl -q net.ipv4.tcp_invalid_ratelimit=800# sysctl -a | grep invalidnet.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`

Wireshark TS | 服务器发送 SYN/ACK 后又收到 SYN 如何处理

Wireshark TS | 服务器发送 SYN/ACK 后又收到 SYN 如何处理
往期推荐

1. Wireshark 提示和技巧 | 捕获点之 TCP 三次握手
2. Wireshark 提示和技巧 | a == ${a} 显示过滤宏
3. Wireshark TS | 当超时或快速重传遇到零窗口
4. Wireshark TS | AWS 服务雪崩效应
5. 网络设备 MTU MSS Jumboframe 全解

 

后台回复「TT」获取 Wireshark 提示和技巧系列 合集
后台回复「TS」获取 Wireshark Troubleshooting系列 合集
如需交流,可后台直接留言,我会在第一时间回复,谢谢!
Wireshark TS | 服务器发送 SYN/ACK 后又收到 SYN 如何处理

原文始发于微信公众号(Echo Reply):Wireshark TS | 服务器发送 SYN/ACK 后又收到 SYN 如何处理

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年7月29日15:21:02
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Wireshark TS | 服务器发送 SYN/ACK 后又收到 SYN 如何处理https://cn-sec.com/archives/3009574.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息