半连接与全连接队列
参考:《UNIX网络编程》、hping3命令使用、小林网络、51CTO
内核为每个监听套接字维护两个队列:未完成连接队列和已完成连接队列 。前者又称半连接队列、SYN队列,后者又称全连接队列、accept 队列。
半连接队列:服务器收到客户端发来的 SYN 报文后,将该连接存入此队列中。这些连接处于 SYN_RCVD 状态。
全连接队列:服务端收到第三次握手的 ACK 后,该连接被内核从半连接队列转移到全连接队列,此时已经完成三次握手,处于 ESTABLISHED 状态。当调用 accept 函数时,该连接将从全连接队列中移除。
backlog
“backlog的含义从未有过正式的定义”,不同的 UNIX 操作系统对其有不同的实现 。这里只以笔者的环境 Ubuntu 16.04 进行说明。
其他版本的相关说明请参见《UNP》第 3 版 84 页。
先说结论:
在 Linux 内核 2.2 之后,backlog
参数影响全连接队列的长度, tcp_max_syn_backlog
作为系统变量则影响半连接队列的长度 。
为什么说“影响”而不是“决定”?因为两个队列长度的具体定义是采用的如下方式:
全连接队列长度 = min(somaxconn, backlog)+1
- 半连接队列的长度计算较为复杂,且不同版本的操作系统实现不一样,故没有必要掌握其计算方法。只需要知道,半连接队列长度可能同时取决于
backlog
、somaxconn
和tcp_max_syn_backlog
,这三者越大,则半连接队列容量越大 。
somaxconn
是 Linux 内核的参数,默认值是 128,可以通过/proc/sys/net/core/somaxconn
来设置其值。
下面就全连接队列进行 backlog
的实验。
1 | ////======!!!代码中的Bind、Listen等函数是博主自己包装的,读者可以直接改成小写的形式!!!======= |
编译并运行以上代码,结果如下:
netstat
分别以客户端和用户端为角度输出了结果,所以有 20 个条目,实际上是 10 条连接,我们只需要看一个纵列的红色条目即可 。可以看到,10 条连接中,只有 6 条是 ESTABLISHED 状态,剩下 4 条是 SYN_SENT 。很奇怪,我们指定的 backlog
是 5,所以不应该只有 5 条是 ESTABLISHED 状态吗?嗯,这里挺坑的,这是因为大多数操作系统的实现都为 backlog
引入了 模糊因子 ,我们这里的模糊因子就是 +1,也就是说,实际全连接队列容量 = backlog + 1
。其他操作系统的模糊因子参考:
关于更多模糊因子的描述,参见《UNP》85~87 页。
接着我们查看 somaxconn
的值:
1 | $ cat /prog/sys/net/core/somaxconn |
将其修改为 3:
1 | //必须在管理员权限下才能修改 |
然后再运行服务器和客户端,得到以下结果:
可见,有 4 条连接处于已建立状态,比我们指定的 somaxconn
还要多 1 。
综上,我们得出结论:全连接队列长度 = min(somaxconn, backlog) + 1
当全连接队列满了怎么办
默认行为是直接丢弃,我们也可通过指定 tcp_abort_on_overflow
参数来调整其行为:
- 0 :直接忽略客户端发过来的 ACK ;
- 1 :回复 RST 给客户端,表示终止本次连接;
1 | $ echo 1 > /prog/sys/net/ipv4/tcp_abort_on_overflow |
一般情况下默认为 0 即可,这是更合理的方式,因为全连接队列已满的状态只是暂时的,客户端会因迟迟未收到 ACK 重发报文,期待不久就能等到全连接队列腾出可用空间。
半连接队列的长度受多方面影响,且不同操作系统版本的实现方式各不相同 ,所以无需掌握具体计算方式。这里笔者简单演示如何间接查看半连接队列的长度。
半连接队列中的状态为 SYN_RCV ,所以我们可以发起 SYN 泛洪,向端口发送大量虚假的 SYN 报文,然后查看该端口上最多时有多少个连接处于 SYN_RCV 状态,此时就是半连接队列的最大长度。
-
下载 hping3 ,用来发起 SYN-flood 攻击
1
sudo apt-get install hping3
-
修改系统配置:
1
2tcp_max_syn_backlog = 128
somaxconn = 150笔者将 backlog 设置为 9(那么全连接队列的容量就为 10),读者随意。
-
运行服务器端
-
发起攻击:
1
2
3
4
5
6
7
8
9hping3 -c 1000 -d 120 -S -w 64 -p 12345 --flood --rand-source 127.0.0.1
//-c 1000 = 发送的数据包的数量
//-d 120 = 发送到目标机器的每个数据包的大小,单位是字节
//-S = 只发送 SYN 数据包
//-w 64 = TCP 窗口大小
//-p 12345 = 目的地端口为12345
//–flood = flood攻击模式
//--rand-source 源IP随机
//目标IP为主机127.0.0.1 -
查看 SYN_RECV 的个数:
1
2netstat -nat | grep :12345 | grep SYN_RECV | wc -l
9可见,半连接队列长度等于 backlog
-
再将 backlog 设置为 200,重复以上操作:
1
2netstat -nat | grep :12345 | grep SYN_RECV | wc -l
150可见,在笔者环境下(Ubuntu 16.04) ,如果 backlog 小于
somaxconn
,则半连接队列容量为 backlog,反之则为somaxconn
,与tcp_max_syn_backlog
无关。
综上, 半连接队列容量可能同时受 tcp_max_syn_backlog
、somaxconn
和 backlog
的影响,想要增大半连接队列的长度,就需要同时增大这三个参数,仅增大 tcp_max_syn_backlog
是无效的 。
对于半连接队列,更多的是需要掌握它与 SYN 泛洪的关系。关于 SYN 泛洪,详细可参见博主另一篇文章-SYN泛洪攻击
另外,还可以从 ss
命令直接观察全连接队列的大小:
1 | $ ss -nlt |
1 | $ ss -nt |
-n 不解析服务名称
-t 只显示 tcp sockets
-l 显示正在监听(LISTEN)的 sockets
对于 LISTEN 状态的 socket
- Recv-Q:当前全连接队列的中的连接个数,即已完成三次握手等待应用程序 accept 的 TCP 连接
- Send-Q:全连接队列的容量
对于非 LISTEN 状态的 socket
- Recv-Q:已收到 但未被应用程序读取 的字节数
- Send-Q:已发送 但未收到确认 的字节数
注意上面的输出,LISTEN 状态下的 Recv-Q ,即全连接队列中的连接个数为 11;但 Send-Q,即全连接的最大长度才为 10,这是怎么回事?这可能是因为 Send-Q 没有包含模糊因子,直接等同于 backlog 。
另外注意,ss
命令和 netstat
命令对 LISTEN 状态输出的 Recv-Q 和 Send-Q 的含义不相同 :
1 | $ netstat -at |
这两条命令是在同一次网络请求下进行的,但两者的 Recv-Q 和 Send-Q 却不相同。个人猜测 netstat
下的 Recv-Q 和 Send-Q 就是单纯的收发字节数,不再表示连接个数或队列长度。
本文结束。