从源码与实战分析TCP半连接队列溢出故障

2024-05-02 07:49:47 浏览数 (2)

文章总结-TCP 三次握手应该这么学

《深入解析TCP连接管理:三次握手与队列溢出应对策略》

图片图片

我们先对文章内容的总结:

TCP三次握手过程:

  1. 客户端发送SYN:客户端调用connect系统调用,内核将套接字状态设置为TCP_SYN_SENT,并发送SYN报文。此时,内核创建request_sock结构,表示半连接请求。
  2. 服务端响应SYN-ACK:服务端收到SYN报文后,内核状态变为TCP_NEW_SYN_RECV,准备SYN-ACK报文响应客户端。
  3. 客户端完成握手:客户端收到SYN-ACK后,内核更新状态为TCP_ESTABLISHED,连接建立,客户端可以开始发送数据。

TCP队列管理:

  • 半连接队列(SYN queue):客户端发送SYN报文后,服务器接收进入SYN_RECV状态,连接被放入半连接队列。队列长度由tcp_max_syn_backlognet.core.somaxconnlisten(fd, backlog)backlog三者最小值决定。
  • 全连接队列(ACCEPT queue):客户端发送ACK报文后,服务器将连接从半连接队列移动到全连接队列,进入ESTABLISHED状态。队列长度由net.core.somaxconnlisten(fd, backlog)backlog两者最小值决定。

连接队列异常处理:

  • 半连接队列已满:服务器无法处理新的SYN请求,导致新的连接尝试失败。可以通过调整net.core.somaxconntcp_max_syn_backlog参数来增加队列大小。
  • 全连接队列已满:服务器已建立连接,但应用程序未及时调用accept(),导致新连接无法被接受。可以通过调整somaxconn参数来增加队列大小,并根据tcp_abort_on_overflow参数决定是丢弃ACK包还是发送RST包给客户端。

压测工具-hping3:多功能的网络性能测试工具

    hping3是一个基于C语言编写的网络性能测试工具,由Salvatore Sanfilippo开发。它能够模拟各种类型的网络包,对服务器进行压力测试,并提供丰富的选项来定制测试。hping3不仅适用于HTTP协议,还支持TCP、UDP、ICMP等多种协议,使其成为一个多功能的网络性能测试工具。

hping3的特点

  1. 多功能性:hping3支持多种网络协议,可以用于测试TCP、UDP、ICMP和RAW-IP协议的性能。
  2. 灵活性:用户可以自定义数据包的内容,包括头部信息和数据负载,以模拟各种网络条件。
  3. 实时性:hping3能够实时显示传输的包数、丢失的包数、往返时间等关键性能指标。
  4. 易于使用:hping3的命令行界面直观,用户可以通过简单的命令行参数进行压力测试。
  5. 强大的脚本功能:支持使用脚本语言(如Perl)来编写测试脚本,实现复杂的测试逻辑。

hping3的使用方法

hping3的使用非常灵活,基本的命令格式如下:

代码语言:javascript复制
hping3 [options] destination

其中,[options]是可选的参数,destination是目标服务器的地址。下面是一些常用的选项:

  • -2--tcp:模拟TCP连接。
  • -4--udp:模拟UDP数据包。
  • -I--interface:指定网络接口。
  • -i--interval:设置数据包发送间隔。
  • -p--port:设置目标端口。
  • --flood:尽可能快地发送数据包。
  • -c--count:发送指定数量的数据包。

例如,要对一个Web服务器的80端口进行TCP压力测试,可以使用以下命令:

代码语言:javascript复制
hping3 --tcp -p 80 -c 1000 example.com

这个命令将会模拟1000个TCP连接到example.com的80端口。

hping3的应用场景

hping3由于其多功能性,可以用于多种网络性能测试场景:

  • 网络带宽测试:通过发送大量UDP数据包测试网络带宽。
  • 网络延迟测试:利用ICMP协议测试网络延迟。
  • 端口扫描:检查目标服务器上开放的端口。
  • 压力测试:模拟大量并发连接,测试服务器的承载能力。
  • 安全测试:模拟恶意流量,测试防火墙和入侵检测系统。

    hping3是一个功能强大的网络性能测试工具,它为网络管理员和安全专家提供了一个强大的测试平台,以评估网络设备和应用程序的性能和安全性。

实战 - TCP 半连接队列溢出

半连接队列长度的计算

“SYN队列”并不是真正的队列,而是将两条信息组合起来作为队列:ehash:这是一个哈希表,保存所有 ESTABLISHED 和 SYN_RECV 状态连接;Accept队列(struct request_sock_queue)中的qlen字段:“SYN队列”中的连接数,实际上是ehash中SYN_RECV状态的连接数。监听socket的struct sock中的sk_ack_backlog保存着accept队列中的连接数。

在深入探讨半连接队列最大长度控制之前,我们首先需要理解几个关键概念:

  1. 半连接队列(SYN Queue):TCP连接的三次握手过程中,服务器在收到客户端的SYN包后,会创建一个半连接队列,用于存放那些已经收到SYN包,但尚未收到客户端ACK包的连接请求。
  2. 全连接队列(ACCEPT Queue):当服务器收到客户端的ACK包后,半连接队列中对应的连接请求会被移动到全连接队列,等待应用程序的accept()调用来完成连接的最终建立。
  3. listen()函数的backlog参数:在调用listen()函数时,可以指定一个backlog参数,它表示全连接队列的最大长度。
  4. /proc/sys/net/core/somaxconn:这个参数定义了系统中所有监听套接字可以接受的最大半连接数。
  5. /proc/sys/net/ipv4/tcp_max_syn_backlog:这个参数定义了每个TCP套接字可以接受的最大半连接数。

    半连接队列最大长度控制的最大大小不是直接由内核参数决定的,而是受net.ipv4.tcp_max_syn_backlog和net.core.somaxconn等参数的影响。这些参数的影响可能因不同的操作系统而异。在CentOS中,SYN队列的最大大小有以下计算公式:

代码语言:javascript复制
Max SYN Queue Size = roundup_pow_of_two(  max(min(somaxconn, backlog, sysctl_max_syn_backlog), 8)   1)

    roundup_pow_of_two(Num) 表示将 Num 向上舍入到 2 次方。例如,当 Num 为 6、7 或 8 时,roundup_pow_of_two(Num) 始终返回 8。

    因此,如果我们将 somaxconn 设置为 64,tcp_max_syn_backlog 设置为 128,listen() 函数的 backlog 设置为 256,那么 CentOS 中 SYN 队列的最大大小将为 256。

    在Ubuntu中,SYN队列的大小必须小于接受队列的最大大小,并且小于或等于net.ipv4.tcp_max_syn_backlog的0.75倍。我们可以将其转换为以下公式:

代码语言:javascript复制
Max SYN Queue Size = min(min(somaxconn, backlog), 0.75 * tcp_max_syn_backlog   1)

如果我们将 somaxconn 设置为 64,tcp_max_syn_backlog 设置为 512,backlog 设置为 256,则接受队列的最大大小为 64,小于 tcp_max_syn_backlog 的 0.75 倍,因此 SYN 队列的最大大小为 64。

如果我们将 somaxconn 设置为 1024,tcp_max_syn_backlog 设置为 256,backlog 设置为 512,则接受队列的最大大小为 512,大于 tcp_max_syn_backlog,因此 SYN 队列的最大大小为 193。

确定backlog的初始值

    首先,我们需要确定调用listen()时传入的backlog参数。在 Nginx 中,listen() 函数的 backlog 参数默认值是 511。这个值定义了 Nginx 监听套接字可以接受的最大半连接数。然而,值得注意的是,这个 backlog 参数的大小最终会受到内核参数 net.core.somaxconn 的限制,该内核参数定义了系统中所有监听套接字可以接受的最大半连接数,默认值通常是 128。

    因此,尽管 Nginx 默认的 backlog 参数设置为 511,实际可使用的最大值可能会受到 somaxconn 内核参数的影响。

半连接队列的查看

    TCP半连接队列的长度 不能用全连接队列一样使用ss命令直接查看,但是我们可以根据TCP半连接的特点-SYN_RECV 状态的 TCP 连接,来统计系统当前TCP半连接队列的长度。

故障模拟

在深入的分析之前我们先进行故障的模拟

当前系统参数

就按照上面计算的参数设置

vim /etc/sysctl.conf

代码语言:javascript复制
net.core.somaxconn=64net.ipv4.tcp_max_syn_backlog = 512net.ipv4.tcp_syncookies = 0

net.ipv4.tcp_syncookies 这个参数很重要,将在后面进行讲解

sysctl -p

代码语言:javascript复制
sysctl -pnet.core.somaxconn = 64net.ipv4.tcp_max_syn_backlog = 512net.ipv4.tcp_syncookies = 0

vim /etc/nginx/conf.d/bingo.conf

代码语言:javascript复制
listen 8080 default backlog=256;

reload nginx

代码语言:javascript复制
nginx -s reloadss -lnp | grep 8080tcp   LISTEN 0      64                                        0.0.0.0:8080             0.0.0.0:*    users:(("nginx",pid=12817,fd=6),("nginx",pid=12816,fd=6))

模拟SYN攻击

代码语言:javascript复制
#-S:这个选项指定hping3发送的是TCP SYN包。TCP SYN包用于建立TCP连接的第一阶段,即同步序列编号。#-p 8080:这个选项指定源端口为8080#--flood:这个选项允许hping3以尽可能快的速度发送数据包,不关心关于往返时间或数据包丢失的信息。这是一种“洪水”模式,用于测试目标系统的处理能力。hping3 -S -p 8080  --flood 192.168.222.169HPING 192.168.222.169 (ens33 192.168.222.169): S set, 40 headers   0 data byteshping in flood mode, no replies will be shown

查看系统当前

代码语言:javascript复制
root@adming-virtual-machine:~# netstat -antup | grep SYN_RECV |wc -l64root@adming-virtual-machine:~# netstat -antup | grep SYN_RECV |wc -l64root@adming-virtual-machine:~# netstat -antup | grep SYN_RECV |wc -l34root@adming-virtual-machine:~# netstat -antup | grep SYN_RECV |wc -l63root@adming-virtual-machine:~# netstat -antup | grep SYN_RECV |wc -l64root@adming-virtual-machine:~# netstat -antup | grep SYN_RECV |wc -l64

可以看到跟上面我们计算的一样,半连接的最大队列长度为64

因为半连接队列溢出被丢弃连接

代码语言:javascript复制
root@adming-virtual-machine:~# netstat -s | grep -i listen    51769826 SYNs to LISTEN sockets droppedroot@adming-virtual-machine:~# netstat -s | grep -i listen    51769862 SYNs to LISTEN sockets droppedroot@adming-virtual-machine:~# netstat -s | grep -i listen    51769871 SYNs to LISTEN sockets droppedroot@adming-virtual-machine:~# netstat -s | grep -i listen    51769876 SYNs to LISTEN sockets droppedroot@adming-virtual-machine:~# netstat -s | grep -i listen    51769898 SYNs to LISTEN sockets dropped

    上面输出的数值是累计值,表示共有多少个 TCP 连接因为半连接队列溢出而被丢弃。隔几秒执行几次,如果有上升的趋势,说明当前存在半连接队列溢出的现象

tcp_syncookies

    SYN队列溢出时服务器的行为主要由该net.ipv4.tcp_syncookies选项决定。

    由于SYN队列的大小总是有限的,一些攻击者可能会尝试通过发送大量SYN数据包来攻击服务器,试图耗尽服务器的SYN队列以阻止合法连接的建立。这通常称为 SYN 洪水攻击。

    SYN Cookie机制就是为了解决这个问题而诞生的。简单来说,启用该机制后,Linux在收到SYN包时会根据时间戳、四元组等信息计算出一个Cookie,然后作为SYN-ACK包的序列号返回给客户端。客户端返回ACK中的序号加一,服务器只需要减一就可以反转原来的Cookie。因此,服务器不再需要将连接请求放入SYN队列中。

图片图片

不过,SYN Cookie机制也有一些缺点:

  • Cookie 的计算不包括 SACK(选择性确认)和 Window Scale 等 TCP 选项,并且服务器在启用 SYN Cookie 后不会保存这些选项,因此无法使用这些功能。虽然从 Linux 内核 v2.6.26 开始,我们可以启用 TCP Timestamps 选项(net.ipv4.tcp_timestamps),并使用 32 位时间戳的低 6 位来存储这些 TCP 选项,但 TCP Timestamps 需要客户端和服务器的共同支持才能实现。真正启用。
  • 用于生成Cookie的Hash计算增加了服务器的负载。

因此,该net.ipv4.tcp_syncookies选项当前具有三个可能的值:

  • net.ipv4.tcp_syncookies = 0,表示关闭SYN Cookie机制。如果SYN队列已满,新到达的SYN报文将被丢弃。
  • net.ipv4.tcp_syncookies = 1,这意味着SYN Cookie机制只有在SYN队列满时才正式启用。
  • net.ipv4.tcp_syncookies = 2,表示无条件启用SYN Cookie机制。

    在不同类型和版本的操作系统中, 的默认值net.ipv4.tcp_syncookies可能不同。我们可以运行以下命令来检查当前值:

代码语言:javascript复制
sysctl -n net.ipv4.tcp_syncookies

    考虑到 SYN Cookie 的潜在副作用,我们一般建议仅在 SYN 队列满时(设置net.ipv4.tcp_syncookies为 1)才启用 SYN Cookie,并优先考虑尽可能增加 SYN 队列的最大大小。运行以下命令修改该选项:

代码语言:javascript复制
sysctl -w net.ipv4.tcp_syncookies=1

判断是否 Drop SYN 请求

    当客户端向服务器端发起一个 SYN 消息以初始化 TCP 连接时,服务器端会将这个待完成的连接请求放入半连接队列(也称为 SYN 队列)。如果服务器端检测到该队列已达到其最大容量,那么它将拒绝(即丢弃)新的连接请求。

    至于服务器端如何判断半连接队列是否达到其承载极限,这不仅涉及到控制半连接队列长度的参数,还与特定的内核参数有关,即 /proc/sys/net/ipv4/tcp_syncookies。这个参数的作用主要是为了防御 SYN Flood 这类拒绝服务攻击的手段。SYN Flood 攻击通过发送大量的 SYN 请求来耗尽服务器的半连接队列,从而使得合法的用户无法建立新的连接。

    tcp_syncookies 在半连接队列满了之后起作用,允许服务器使用一种特殊的机制来处理新的 SYN 请求,即便队列已满。这种方式不需要为每个半连接分配一个完整的数据结构,而是使用一种简化的“cookie”来快速验证连接请求,从而允许合法的连接在半连接队列溢出的情况下仍然能够建立。

判断是否 Drop SYN 请求的流程图:

图片图片
  1. 对于半连接队列: 如果服务器的半连接队列已达到其最大容量,并且 tcp_syncookies 功能未被激活,那么服务器将开始丢弃新的连接请求。
  2. 对于全连接队列: 当服务器的全连接队列变得过载,并且存在多于一个的连接请求,这些请求尝试重传 SYN ACK 包以完成连接,但未能成功,这些请求将被服务器丢弃。
  3. 结合 tcp_syncookiesmax_syn_backlog 参数:tcp_syncookies 未启用,且当前半连接队列的长度已经接近 max_syn_backlog 所设置的上限,具体来说,如果 max_syn_backlog 的值减去当前半连接队列的长度的结果小于 max_syn_backlog 值的四分之一(max_syn_backlog >> 2),那么新的 SYN 请求也将被丢弃。

防御 SYN 攻击的策略

    SYN 攻击是一种常见的拒绝服务攻击(DoS),通过发送大量的 SYN 包来耗尽服务器的资源,导致正常的连接请求无法被处理。以下是几种可以采取的策略来防御 SYN 攻击:

扩展半连接队列的容量

  1. 调整 tcp_max_syn_backlog 参数:这个参数限定了服务器可以跟踪的未完成的 TCP 连接的数量。我们可以通过以下命令查看和设置该参数: sysctl net.ipv4.tcp_max_syn_backlog sysctl -w net.ipv4.tcp_max_syn_backlog=1024
  2. 调整 somaxconn 参数somaxconnlisten() 调用中的 backlog 参数的最大值,决定了全连接队列的最大长度。修改它可以通过直接编辑 /etc/sysctl.conf 文件: sudo echo 'net.core.somaxconn = 1024' >> /etc/sysctl.conf sysctl -p /etc/sysctl.conf
  3. 增加 Web 服务器的 backlog 参数:对于 Nginx,我们需要在 Nginx 配置文件中每个 listen 指令后添加 backlog 参数: server { listen 80 backlog=1024; ... } 修改配置后,重启 Nginx 以应用更改: sudo systemctl restart nginx
激活 tcp_syncookies 特性

    启用 tcp_syncookies:当 tcp_max_syn_backlog 参数值的连接队列满了时,syncookies 允许内核用一种更轻量的方式继续处理连接请求。启用它:

代码语言:javascript复制
sysctl -w net.ipv4.tcp_syncookies=1

将该设置写入 /etc/sysctl.conf 以持久化配置:

代码语言:javascript复制
echo 'net.ipv4.tcp_syncookies = 1' | sudo tee -a /etc/sysctl.conf
sysctl -p /etc/sysctl.conf

减少 SYN ACK 包的重传尝试

    调整 tcp_syn_retriestcp_synack_retries 参数:这些参数控制了内核在放弃连接前发送 SYN ACK 包的次数。减少重传次数可以加快连接的超时过程:

代码语言:javascript复制
sysctl -w net.ipv4.tcp_syn_retries=1
sysctl -w net.ipv4.tcp_synack_retries=1

同样,将这些更改添加到 /etc/sysctl.conf 中以持久化:

代码语言:javascript复制
echo 'net.ipv4.tcp_syn_retries = 1' | sudo tee -a /etc/sysctl.conf
echo 'net.ipv4.tcp_synack_retries = 1' | sudo tee -a /etc/sysctl.conf
sysctl -p /etc/sysctl.conf

篇章总结

    在实际生产环境中,半连接队列和全连接队列溢出的问题虽然可能在服务器的监控指标中不显眼,但它们对服务稳定性的潜在威胁却不容忽视。当这些队列溢出时,服务器可能表面上看起来运行正常,如 CPU 使用率、内存使用量和网络连接数等关键指标均显示正常,但实际上客户端的业务请求却可能遭受持续性的干扰。特别是在高负载环境下,如果上游服务采用的是短连接策略,那么这种风险更是会急剧增加。

容易被忽视
  1. 监控盲区:标准的服务器监控系统可能未能覆盖到网络队列状态的监控,导致在问题初期难以被及时发现。
  2. 隐蔽性:这类问题通常不会直接表现为资源耗尽或服务崩溃,而是通过拒绝新连接的方式隐蔽地影响服务。
故障致命
  1. 服务中断:队列溢出可能导致服务器无法接受新的连接请求,从而造成服务中断。
  2. 难以诊断:由于监控指标不明确,问题可能难以快速定位和解决。
  3. 连锁反应:对客户端业务的影响可能引发连锁反应,影响整个业务流程。

推荐阅读博文

https://github.com/torvalds/linux

https://www.emqx.com/en/blog/emqx-performance-tuning-tcp-syn-queue-and-accept-queue

https://arthurchiao.art/blog/tcp-listen-a-tale-of-two-queues/

我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!

0 人点赞